# Agency Guide Source: https://docs.qwairy.co/agencies/agency-guide How agencies run GEO monitoring across multiple client brands in Qwairy — isolation, access, branding, and client reporting in one place. This guide is for agencies and consultancies running GEO monitoring for more than one client brand in Qwairy. It connects the pieces you'll actually reach for: how to isolate each client's account, how much access to give them, how to make the experience look like yours, and how to report back to them. Before onboarding your first client, make three decisions: 1. **Isolation model** — does each client get their own contained account, or do you manage every brand under one team? 2. **Access model** — does the client just view reports, or do they actively manage their own brand? 3. **Branding** — does the client see Qwairy, or does the experience look like your agency's own product? The rest of this guide walks through each decision and the setup that follows from it. *** ## Choosing an Access Model The core decision is how much a client sees and controls. Qwairy gives you two mechanisms, and they serve different situations: | | **Virtual Teams (sub-team)** | **Viewer role** | | -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | **Scope** | Full sub-team: its own members, workspaces, and credit pool | Single brand, read-only | | **Client can** | Actively manage the brand — prompts, competitors, settings | View dashboards and reports only | | **Isolation** | Complete — client never sees the rest of your organization | None — client is a member of your organization, scoped to specific brands | | **Billing** | Inherits your organization's plan; sub-team draws from allocated credits | N/A — no separate credit pool | | **Best for** | Clients who need hands-on management of their own brand | Stakeholders who just need to check numbers | As [Virtual Teams](/documentation/team/virtual-teams) puts it: use a transfer (rather than the Viewer role) when the client needs to actively manage the brand, not just read it. Viewer access stays read-only and brand-scoped within your organization, while a sub-team transfer gives full management in an isolated team. If neither fits — for example, you're a small team managing a handful of client brands yourself, with no client logins at all — you can simply keep everything under one team and control brand access per [team member](/documentation/team/manage-members) instead. Virtual Teams is available on **Business and Enterprise plans**. *** ## Setting Up a Client To give a client a fully isolated, self-service account for their brand: Go to **Team Management > Virtual Teams** and click **Create**. Name it after the client, and allocate a credit amount and workspace quota from your organization's pool. Either tick the workspace under **Move existing workspaces** while creating the sub-team, or move it afterwards with **Move workspace**. The workspace's full monitoring history travels with it — nothing resets. Switch to the client's sub-team and go to **Team Management > Members** to invite them. They'll only ever see workspaces inside that sub-team, never the rest of your organization. Only Owners and Managers of your parent organization can move workspaces, and they retain access to every sub-team automatically without a separate invitation. If you'd rather keep the client's brand inside your own organization and just let a stakeholder check the numbers, skip the sub-team and add them as a [Viewer](/documentation/workspace/viewers) scoped to that one brand instead. *** ## White-Labeling the Experience Once a client is set up, you can make the whole experience look like your agency's own product instead of Qwairy's: Replace the Qwairy logo across the dashboard, shared reports, and login. Serve the dashboard and login from your own subdomain, e.g. `analytics.yourcompany.com`. Hide "Powered by Qwairy" across the dashboard, shared links, and exports. White-labeling is an **Enterprise** capability, configured under **Team Management > White Label**. See [Agencies overview](/agencies/introduction) for the full picture, including fully white-labeled authentication via SSO. *** ## Reporting to Clients Two ways to get results in front of clients, depending on whether they have a Qwairy login: * **[Pitch Audits](/agencies/pitch-audits)**: create pre-sales audits for prospects before onboarding them into a full workspace, with prompt review, AI answer evidence, detected competitors, cited sources, and shareable reports. * **[Shared links](/documentation/workspace/shared-links)** — generate a public URL for a dashboard, brand perception snapshot, GEO Matrix, competitor comparison, or prompt signals view. No Qwairy account needed on the client's end, and links can be set to expire or revoked at any time. * **[Weekly report emails](/agencies/notifications)** — enable the per-workspace weekly report so clients get a recurring summary without checking the dashboard. * **Outbound webhooks** — if your agency has its own reporting systems, route Qwairy's notification events to your own endpoint as signed JSON instead of Qwairy emails. This is an Enterprise capability; see [Developers > Webhooks](/developers/webhooks) for the payload format and setup, which starts in the same **Team Management > Notifications** screen as the weekly report. *** ## Related Pages * [Pitch Audits](/agencies/pitch-audits): pre-sales audits and shareable sales reports * [Virtual Teams](/documentation/team/virtual-teams) — sub-teams, credit allocation, and workspace transfers * [Viewers](/documentation/workspace/viewers) — read-only, brand-scoped access * [Shared Links](/documentation/workspace/shared-links) — public report links for clients without an account * [Agencies Overview](/agencies/introduction) — white-label capabilities at a glance * [Custom Domain Overview](/agencies/custom-domain/overview) — serve Qwairy from your own subdomain * [Developers: Webhooks](/developers/webhooks) — outbound notification events for your own systems # DNS Setup Source: https://docs.qwairy.co/agencies/custom-domain/dns-setup Add your custom domain in White Label settings and point it to Qwairy with a CNAME record at your DNS provider. Setting up a custom domain takes two parts: adding the subdomain in Qwairy, then creating a CNAME record at your DNS provider. ## Add Your Domain in Qwairy Go to **Team Management > White Label** in your Qwairy dashboard. In the custom domain section, enter the subdomain you want to use (e.g. `analytics.yourcompany.com`) and save. Qwairy displays the **CNAME target** to point your subdomain to. Keep this value handy for the next step. Use a subdomain (e.g. `analytics.yourcompany.com`). Apex (root) domains are not supported. ## Create the CNAME Record In your DNS provider (Cloudflare, Route 53, GoDaddy, etc.), add a new **CNAME** record: ``` Type: CNAME Name: analytics (your subdomain) Value: ``` * **Name** — the subdomain you added in Qwairy (for `analytics.yourcompany.com`, the name is usually `analytics`) * **Value** — the CNAME target shown in your White Label settings DNS providers label these fields differently. **Name** may appear as Host or Record, and **Value** may appear as Target, Points to, or Content. Use the exact target shown in your White Label settings. DNS propagation can take some time — usually minutes, but it can take up to 48 hours depending on your provider. ## Next Step Once the record is in place, head to [Verification](/agencies/custom-domain/verification) to confirm your configuration and activate the domain. # Custom Domain Overview Source: https://docs.qwairy.co/agencies/custom-domain/overview Serve the Qwairy dashboard and login from your own subdomain, fully branded — e.g. analytics.yourcompany.com. A custom domain lets you serve the Qwairy dashboard and login from your own subdomain instead of the default Qwairy URL. Your team and clients sign in and work entirely under your brand. ## What a Custom Domain Gives You * **A branded dashboard** — access Qwairy from a subdomain you control, such as `analytics.yourcompany.com` * **A branded login** — your team signs in on your domain, with your [logo](/agencies/logo) on the sign-in page * **A consistent experience** — paired with your logo and [removed branding](/agencies/remove-branding), the product looks and feels like your own The custom domain brands the **sign-in page**, but magic-link emails and the Google consent screen stay Qwairy-branded by default. For fully white-labeled authentication on your domain, connect your own identity provider with [SSO](/documentation/team/sso) — your IdP handles login, and with SSO + JIT provisioning, matching users are added to your workspace automatically on first sign-in. ## Prerequisites You'll need access to your DNS provider to add a record for the subdomain you want to use (e.g. `analytics.yourcompany.com`). Apex (root) domains such as `yourcompany.com` are not supported. Choose a subdomain like `analytics.yourcompany.com` or `ai.yourcompany.com`. Root (apex) domains are not supported. Always use a subdomain. ## How Setup Works Add your domain in White Label settings and create the CNAME record at your DNS provider. Verify your DNS, and the Qwairy team finalizes activation so your team can sign in at your domain. # Verification Source: https://docs.qwairy.co/agencies/custom-domain/verification Verify your DNS configuration to activate your custom domain. Provisioning and the HTTPS certificate are handled automatically so your team can sign in at your domain. Once your CNAME record is in place, verify it in Qwairy to activate your custom domain. ## Verify Your DNS Go to **Team Management > White Label** in your Qwairy dashboard. Next to your custom domain, click **Verify DNS Configuration**. Qwairy checks that your CNAME record points to the correct target. If verification doesn't pass right away, your DNS record may still be propagating. Wait a little and try again — propagation can take up to 48 hours. ## Activation Once your CNAME record is in place, Qwairy provisions your domain automatically and a secure HTTPS certificate is issued for it. Verification confirms everything is live, and your team can then sign in to Qwairy at your domain. Provisioning and the certificate are automatic - there is nothing else to configure on your side. If verification does not pass right away, your DNS may still be propagating; wait a little and try again. ## Signing In on Your Domain A few things to keep in mind once your domain is live: * **Workspace membership is required** — every user who signs in on your domain must be a member of your workspace. * **Independent sessions** — sessions on your domain are separate from sessions on [www.qwairy.co](https://www.qwairy.co). Signing in on one does not sign you in on the other. Pair your custom domain with a [custom logo](/agencies/logo) and [removed branding](/agencies/remove-branding) for a fully branded sign-in and dashboard experience. ## FAQ Confirm the CNAME record's **Name** matches the subdomain you added in Qwairy, and its **Value** matches the target shown in your White Label settings exactly. DNS changes can take up to 48 hours to propagate, so it's worth trying again after a short wait. No. Apex (root) domains are not supported. Use a subdomain such as `analytics.yourcompany.com`. Yes. Every user who signs in on your domain must be a member of your workspace. You can invite them from **Team Management**. Sessions on your custom domain are independent from sessions on [www.qwairy.co](http://www.qwairy.co). Signing in on one surface does not carry over to the other — this is expected. # White-Label Source: https://docs.qwairy.co/agencies/introduction Make Qwairy feel like your own product — custom logo, a branded custom domain for the dashboard and login, and the option to remove Qwairy branding. White-Label lets you present Qwairy as your own product. Upload your logo, serve the dashboard and login from your own subdomain, and remove the "Powered by Qwairy" branding across the experience — so clients and stakeholders see your brand, not ours. ## What's Included Replace the Qwairy logo with your own across the dashboard, shared reports, and your branded login. Serve the dashboard and login from your own subdomain (e.g. `analytics.yourcompany.com`). Hide the "Powered by Qwairy" branding across the dashboard, shared links, and exports. ## Authentication On a custom domain, the **sign-in page is branded** with your logo and name. By default, the sign-in methods themselves stay Qwairy-branded: magic-link emails come from Qwairy and the Google consent screen shows "Qwairy". For **fully white-labeled authentication** — where your users authenticate entirely under your brand and never see Qwairy — connect your own identity provider via [SSO](/documentation/team/sso). Your IdP then handles sign-in, and with enforcement on it becomes the only way your domain's users log in. ## Who It's For White-Label is designed for teams that share Qwairy with clients, partners, or internal stakeholders under their own brand — agencies, consultancies, and organizations that want a fully branded analytics experience. ## How to Get It Enabled White-Label is an **Enterprise** capability. White-Label is available on the **Enterprise plan**. To enable it for your team, talk to your account manager, or [book a demo](https://qwairy.co/demo). Once it's enabled for your team, you'll find the controls under **Team Management > White Label**. ## Explore Upload your logo and learn where it appears. Hide the "Powered by Qwairy" branding. Serve Qwairy from your own subdomain. Add your domain and create the CNAME record. Verify your DNS and activate your domain. Control which emails Qwairy sends, or route events to your own webhook. Run pre-sales prospect audits and share reports before client onboarding. Fully white-label authentication with your own identity provider. # Custom Logo Source: https://docs.qwairy.co/agencies/logo Upload your own logo to replace Qwairy branding across the dashboard, shared reports, and your branded login. Uploading a custom logo replaces the Qwairy logo with your own throughout the experience your team and clients see. ## Upload Your Logo Go to **Team Management > White Label** in your Qwairy dashboard. In the logo section, click **Upload** and select your image file. Your logo is applied right away. ## Accepted Formats | Requirement | Detail | | ---------------- | --------------- | | **File types** | PNG, JPEG, WebP | | **Maximum size** | 5 MB | SVG files are not supported. Upload a PNG, JPEG, or WebP image instead. For the sharpest result, use a high-resolution PNG or WebP with a transparent background. ## Where Your Logo Appears Once uploaded, your logo replaces the Qwairy logo in: * **The dashboard sidebar** — visible to everyone on your team * **Shared report pages** — visible to anyone you share a link with * **Your branded login** — when a [custom domain](/agencies/custom-domain/overview) is set up, your logo appears on the sign-in page To present a fully branded experience, pair your logo with a [custom domain](/agencies/custom-domain/overview) and [remove the Qwairy branding](/agencies/remove-branding). # Notifications Source: https://docs.qwairy.co/agencies/notifications Control which email notifications Qwairy sends, including the per-workspace weekly report. Notifications keep your team informed about changes in AI visibility without checking the dashboard manually. You can control which emails Qwairy sends on a per-workspace basis. ## Weekly Report Each workspace can send a **weekly report** email summarizing its AI visibility. The report is controlled per workspace, so you can enable it where it's useful and turn it off elsewhere. Go to **Workspace > Notifications** in your Qwairy dashboard. Turn the **weekly report** on or off for this workspace. Changes take effect immediately. Notifications are sent to the email address associated with each recipient's Qwairy account. ## Outbound Webhooks Instead of receiving Qwairy emails, a white-label team can have notifications POSTed to its own endpoint as signed JSON events — so you deliver them to your users under your own brand, from your own systems. Outbound webhooks are an **Enterprise** capability, documented in full under [Developers > Webhooks](/developers/webhooks) alongside the payload format, signature verification, and delivery behavior. The setup steps start in the same place as the weekly report: **Team Management > Notifications**. # Pitch Audits Source: https://docs.qwairy.co/agencies/pitch-audits Create pre-sales AI visibility audits for prospects before onboarding them into a full workspace. Pitch Audits are a **pre-sales** workflow for agencies. They help you qualify prospects, prepare sales conversations, and show AI visibility opportunities before creating a full client workspace. Enter a prospect domain, choose the market and AI engines, review the generated buyer prompts, then launch the audit and share the report. Use Pitch Audits when you need to: * show a prospect how AI assistants answer buyer questions in their category * compare the prospect against competitors detected in those answers * surface cited sources, content gaps, and available-to-buy opportunities * share a sales-ready report before a full client onboarding Pitch Audits are available to agency teams. ## Create a Pitch Audit In Qwairy, go to **Pitch Audits**. Add the prospect website and choose the country for the audit. Country selection controls which AI engines are available. Choose how many prompts to generate and which AI engines to run. The default engines are **ChatGPT**, **Gemini**, and **Copilot**. Depending on country availability, you can also select **Perplexity**, **AI Mode**, and **Grok**. Qwairy generates buyer prompts for the selected market. Review, edit, add, or remove prompts before launching answer generation. Confirm the prompts to spend credits and collect AI answers. You can also enable automatic launch after prompt review when creating the audit. ## Credits Credits are charged only when AI answers are generated. Prompt preparation and prompt review do not spend response credits. For Pitch Audits, each selected AI engine costs **1 credit per prompt**. ``` prompt count x selected engines = required credits ``` For example, a 20-prompt audit across the three default engines costs: ``` 20 x 3 = 60 credits ``` ## What the Report Includes Pitch Audit reports are built for pre-sales conversations. A completed report includes: * an AI visibility score for the audited brand * the underlying prompts and answer evidence * cited source domains and source URLs * available-to-buy opportunities when sources can be targeted * Social, Shopping, Local, and Ads/Sponsored insights when detected * multi-competitor reports when competitors are found * public sharing options for client-facing delivery Pitch Audit reports are snapshots. AI assistants can answer differently over time, so create a new audit when you want a fresh market read. ## Share the Report Open the completed report and copy its public link to share it with a prospect or client. Public reports include the same report improvements as the dashboard report, including the detected insight sections when available. If your team uses white-label features, shared reports follow your agency branding settings. ## Related * [Agency Guide](/agencies/agency-guide): set up clients, access, and reporting * [Shared Links](/documentation/workspace/shared-links): share dashboard views without requiring a login * [Providers & Credits](/documentation/get-started/credits): credit costs for monitoring and model usage # Remove Branding Source: https://docs.qwairy.co/agencies/remove-branding Hide the "Powered by Qwairy" branding across the dashboard, shared report links, and exports with a single toggle. Removing the Qwairy branding hides the "Powered by Qwairy" mark so the experience your team and clients see is fully yours. ## Hide the Branding Go to **Team Management > White Label** in your Qwairy dashboard. Turn on the **Remove Qwairy branding** toggle. The change takes effect immediately. ## Where the Branding Is Removed With the toggle enabled, the "Powered by Qwairy" branding is hidden across: * **The dashboard** — no Qwairy mark in the interface * **Shared report links** — clean, unbranded pages for the people you share with * **Exports** — generated files come without Qwairy branding Combine this with a [custom logo](/agencies/logo) and a [custom domain](/agencies/custom-domain/overview) for an end-to-end branded experience. # API Keys Source: https://docs.qwairy.co/developers/api-keys Create and manage API keys for programmatic access to your Qwairy data. API keys allow you to access Qwairy data programmatically through the REST API. Use them to build custom dashboards, automate reports, or integrate Qwairy data into your existing tools. Navigate to **Team Management > API** ("API Access", [https://www.qwairy.co/dashboard/team/api](https://www.qwairy.co/dashboard/team/api)) to manage them. The REST API and API keys are available on **Growth** plans and above. ## Creating an API Key Click **Create API Key**, give it a descriptive name, and confirm. The key is displayed once — copy it immediately and store it securely. API keys are shown only once at creation. If you lose a key, delete it and create a new one. ## API Keys Table | Column | Description | | ----------- | -------------------------------- | | **Name** | Key label | | **Key** | Masked key value (click to copy) | | **Created** | Creation date | | **Actions** | Delete button | ## Deleting a Key Click the delete icon and confirm. Any integrations using this key will immediately stop working. ## Using API Keys Include your API key in the `Authorization` header of API requests: ```bash theme={null} curl -H "Authorization: Bearer YOUR_API_KEY" \ https://www.qwairy.co/api/v1/brands ``` See the [API Reference](/developers/introduction) for full endpoint documentation. # Authentication Source: https://docs.qwairy.co/developers/authentication Learn how to authenticate with the Qwairy API using Bearer tokens All API requests require a Bearer token in the `Authorization` header. Tokens are scoped to your team and provide access to all brands within your account. ## Getting Your API Token Go to your [Team Management > API Access](https://qwairy.co/dashboard/team/api). Click **Create API Token** and give it a descriptive name (e.g., "Marketing Dashboard", "Data Warehouse"). Your token will be shown **only once**. Copy it immediately and store it securely. **Keep your tokens secure!** Never commit tokens to version control or share them publicly. If a token is compromised, revoke it immediately from **Team Management > API Access**. ## Using Your Token Include the token in the `Authorization` header with the `Bearer` prefix: ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands" \ -H "Authorization: Bearer qw-api-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ### Token Format All Qwairy API tokens follow this format: ``` qw-api-[32 hexadecimal characters] ``` Example: `qw-api-a1b2c3d4e5f6789012345678abcdef01` ## Authentication Errors Authentication failures return HTTP `401` with a flat body — `error` is a short status label and `message` explains the failure: | Condition | `message` | | ------------------------------------------------------------------ | -------------------------------------------------------------------------------- | | Missing or malformed `Authorization` header (not `Bearer `) | `Missing or invalid Authorization header. Use: Authorization: Bearer qw-api-xxx` | | Invalid or revoked token, or plan below Growth | `Invalid API token or insufficient subscription plan (Growth+ required)` | ### Example Error Response ```json theme={null} { "error": "Unauthorized", "message": "Missing or invalid Authorization header. Use: Authorization: Bearer qw-api-xxx" } ``` See [Error Codes](/developers/errors) for the full list of error shapes, including the nested shape used by resource (validation and not-found) errors. ## Best Practices Store tokens in environment variables, not in code. ```bash theme={null} export QWAIRY_API_TOKEN="qw-api-xxx" ``` Name tokens by their purpose for easy management. Delete and recreate tokens periodically for security. Create different tokens for different integrations. ## Managing Tokens You can manage your API tokens from [Team Management > API Access](https://qwairy.co/dashboard/team/api): * **View tokens**: See all active tokens with their last usage date * **Delete tokens**: Revoke access immediately by deleting a token * **Create new tokens**: Generate new tokens as needed There's no limit to the number of tokens you can create, but we recommend keeping only the tokens you actively use. # List Actions Source: https://docs.qwairy.co/developers/endpoints/actions/list GET /api/v1/brands/{brandId}/actions List the Action Center's prioritized recommendation queue Returns the Action Center's deterministic recommendation queue for a brand: prioritized actions with a 3-tier label, a 0-100 priority score, a 1-5 demand badge, the quantified "why", the raw signals, and a step-by-step checklist. **Private beta.** The Action Center must be enabled for your team; otherwise this endpoint returns `403`. Contact your account manager to request access. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Filter by status: `SUGGESTED` (awaiting triage), `PENDING`, `IN_PROGRESS`, `COMPLETED`, `DISMISSED`, `RESOLVED` (signal disappeared, auto-closed). Defaults to the live queue: `SUGGESTED` + `PENDING` + `IN_PROGRESS`. Filter by action type: `CONTENT_GAP`, `CONTENT_OPTIMIZE`, `TECHNICAL_FIX`, `SCHEMA_ADD`, `AUTHORITY_BUILD`, `SOURCE_OUTREACH`, `COMPETITOR_ANALYSIS`, `PERCEPTION_FIX`, `SEARCH_TARGET`, `SOCIAL_ENGAGE`, `PRODUCT_OPTIMIZE`, `LOCAL_OPTIMIZE` Filter by category: `TECHNICAL`, `CONTENT`, `AEO`, `AUTHORITY`, `VISIBILITY` Maximum number of actions to return (max: 100) Pagination offset ### Response Actions ordered by priority (highest first), then creation date. Unique identifier of the action Action type (see the `type` filter for values) Action category Detector family that produced the action (e.g. `PAGE_ISSUE`, `QUESTION_GAP`, `SOURCE_OPPORTUNITY`, `SEARCH_QUERY`) Nature of the work: `ON_SITE`, `OFF_SITE` or `SETUP` Action title What to do and why External URL backing the action, when applicable AI provider this action is scoped to (null = brand-level) Priority tier: `CRITICAL`, `HIGH` or `STANDARD` Tier-anchored priority score from 0 to 100 (Critical 70-100, High 40-69, Standard below 40), refined by demand, effort and engine scope. Higher means act sooner; it never contradicts the tier. Demand signal from 1 to 5 High expected impact for low effort Estimated workload: `LOW`, `MEDIUM` or `HIGH` Lifecycle status Raw quantified signals behind the priority (citations, fan-out occurrences, impressions, competitive pressure, affected pages...) The quantified "why it matters" sentence Explanation of the estimated effort Metric value when the action was created (outcome measurement) Target metric value, when applicable Linked monitored prompt, when applicable Step-by-step execution guide ISO 8601 creation date ISO 8601 last update date `total`, `limit`, `offset`, `hasMore` ```bash cURL theme={null} curl "https://www.qwairy.co/api/v1/brands/{brandId}/actions?status=SUGGESTED&limit=10" \ -H "Authorization: Bearer qw-api-xxx" ``` ```json Response theme={null} { "data": [ { "id": "a1b2c3d4-...", "type": "SOURCE_OUTREACH", "category": "AUTHORITY", "sourceType": "SOURCE_OPPORTUNITY", "scope": "OFF_SITE", "title": "Get cited by: TechRadar", "description": "AI answers cite this source 16 times, but it doesn't mention your brand. Outreach could secure valuable citations.", "sourceUrl": "https://techradar.com", "targetProvider": null, "forceTier": "HIGH", "priorityScore": 53, "demandBadge": 3, "isQuickWin": false, "effort": "MEDIUM", "status": "SUGGESTED", "signals": { "citations": 16, "sourceCategory": "MEDIA", "competitivePressure": 2 }, "impactJustification": "Source cited 16x by AI answers without mentioning the brand.", "effortJustification": "Outreach: research the contact, pitch, and follow up.", "baselineMetric": null, "targetMetric": null, "questionId": null, "checklist": [ "Review what content the source already cites", "Prepare a superior resource or expert angle", "Pitch the editorial contact", "Track the citation when secured" ], "createdAt": "2026-06-12T05:00:00.000Z", "updatedAt": "2026-06-12T05:00:00.000Z" } ], "pagination": { "total": 42, "limit": 10, "offset": 0, "hasMore": true } } ``` # Get Answer Source: https://docs.qwairy.co/developers/endpoints/answers/get GET /api/v1/brands/{brandId}/answers/{answerId} Retrieve a single AI response with full text and detailed analysis Retrieve the complete response with full text, detected competitor mentions, cited sources, and sentiment. ### Path Parameters The unique identifier of the brand The unique identifier of the answer ### Response Indicates if the request was successful Answer ID Associated prompt info (id, text, topic, type) AI provider name AI model name Full response text List of mentioned competitors with position, relationship, and sentiment List of cited sources with URL, domain, position, and isSelf flag Overall sentiment score Generation timestamp ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/answers/ans1" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "answer": { "id": "ans1", "prompt": { "id": "q1", "text": "What are the best products in this category?", "topic": "Product Reviews", "type": "TOFU" }, "provider": "ChatGPT", "model": "GPT-4o", "text": "Based on recent reviews and comparisons, several products stand out in this category. My Brand offers one of the best solutions with excellent performance and user ratings. Competitor A also provides a strong alternative...", "competitors": [ { "name": "My Brand", "position": 3, "relationship": "SELF", "sentiment": 85 }, { "name": "Competitor A", "position": 1, "relationship": "DIRECT", "sentiment": 80 } ], "sources": [ { "url": "https://industry-news.com/product-comparison", "domain": "industry-news.com", "position": 1, "isSelf": false }, { "url": "https://mybrand.com/products", "domain": "mybrand.com", "position": 2, "isSelf": true } ], "sentiment": 82, "createdAt": "2024-12-19T10:00:00.000Z" } } ``` ### Error Responses | Status | Code | Description | | ------ | ------------------- | ------------------------------------- | | 400 | `INVALID_PARAMETER` | Invalid query parameter | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | | 404 | `ANSWER_NOT_FOUND` | Answer doesn't exist | # List Answers Source: https://docs.qwairy.co/developers/endpoints/answers/list GET /api/v1/brands/{brandId}/answers Retrieve AI-generated responses with filters for provider, brand mentions, and sources Answers are the AI-generated responses to your monitored prompts. Each answer includes the full text, detected brand mentions with position and sentiment, and cited sources. Filter by provider, topic, or presence of brand mentions. See [Entities](/developers/entities#answer) for the complete Answer object structure, including [CompetitorMention](/developers/entities#competitormention) and [SourceCitation](/developers/entities#sourcecitation) details. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by prompt type: `TOFU`, `MOFU`, `BOFU` Filter by presence of brand mention: `true` or `false` Filter by presence of brand source citation: `true` or `false` Maximum number of answers to return (max: 100) Pagination offset Field to sort by: `createdAt` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of answers matching filters Number of answers in this response Maximum items per page Number of items skipped Answer ID Associated prompt ID Associated prompt text AI provider name AI model name First 200 characters of the response Whether brand is mentioned Position in mention list (if mentioned) Whether brand domain is cited Number of competitors mentioned Number of sources cited Sentiment score (0-100) Generation timestamp (ISO 8601) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/answers?hasSelfMention=true&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 312, "count": 1, "limit": 10, "offset": 0 }, "answers": [ { "id": "ans1", "promptId": "q1", "promptText": "What are the best products in this category?", "provider": "ChatGPT", "model": "GPT-4o", "textPreview": "Based on recent reviews and comparisons, several products stand out in this category. My Brand offers one of the best solutions...", "hasSelfMention": true, "selfMentionPosition": 3, "hasSelfSource": false, "competitorsCount": 5, "sourcesCount": 3, "sentiment": 82, "createdAt": "2024-12-19T10:00:00.000Z" } ] } ``` ## Filtering Tips ### Find answers where you're mentioned ```bash theme={null} GET /api/v1/brands/{brandId}/answers?hasSelfMention=true ``` ### Find answers where your content is cited ```bash theme={null} GET /api/v1/brands/{brandId}/answers?hasSelfSource=true ``` ### Combine filters for specific insights ```bash theme={null} # ChatGPT responses where brand is mentioned but not cited GET /api/v1/brands/{brandId}/answers?provider=chatgpt&hasSelfMention=true&hasSelfSource=false ``` # Get Brand Source: https://docs.qwairy.co/developers/endpoints/brands/get GET /api/v1/brands/{brandId} Retrieve detailed information about a specific brand Retrieve detailed information about a specific brand, including aggregate statistics for prompts, answers, and competitors. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique brand identifier ### Response Indicates if the request was successful Unique brand identifier (UUID) Brand display name Primary domain for the brand Brand description (can be null) ISO 8601 creation timestamp Number of prompts tracked for this brand Total number of AI responses analyzed Number of competitors (SELF + DIRECT) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com", "description": "Leading provider of innovative solutions", "createdAt": "2024-01-15T10:30:00.000Z", "stats": { "promptsCount": 156, "answersCount": 312, "competitorsCount": 8 } } } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # List Brands Source: https://docs.qwairy.co/developers/endpoints/brands/list GET /api/v1/brands Retrieve all brands accessible to your team Brands are the core entities in Qwairy. Each brand represents a company or product you monitor across AI platforms. This endpoint returns every brand your team can access. See [Entities](/developers/entities#brand) for the complete Brand object structure. Bearer token. Example: `Bearer qw-api-xxx` ### Response Indicates if the request was successful Total number of brands Unique brand identifier (UUID) Brand display name Primary domain for the brand ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "total": 2, "brands": [ { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com" }, { "id": "cm0987654321fedcba", "name": "My Other Brand", "domain": "myotherbrand.com" } ] } ``` ## Next Steps Once you have your brand IDs, you can: * [Get performance metrics](/developers/endpoints/performance) * [List competitors](/developers/endpoints/competitors/list) * [View source domains](/developers/endpoints/source-domains/list) * [View source URLs](/developers/endpoints/source-urls) * [Browse prompts](/developers/endpoints/prompts/list) # Competitor Evolution Source: https://docs.qwairy.co/developers/endpoints/competitors/evolution GET /api/v1/brands/{brandId}/competitors/{competitorId}/evolution Track how a competitor's metrics change over time Track how a competitor's metrics change over time. Returns daily time-series data for mentions, share of voice, and sentiment. ### Path Parameters The unique identifier of the brand The unique identifier of the competitor ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/competitors/cmp1/evolution?period=7" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "competitor": { "id": "cmp1", "name": "My Brand", "relationship": "SELF" }, "evolution": [ { "date": "2024-12-01", "mentions": 5, "shareOfVoice": 7.2, "avgPosition": 2.3, "avgSentiment": 76.5 }, { "date": "2024-12-02", "mentions": 8, "shareOfVoice": 8.5, "avgPosition": 2.1, "avgSentiment": 78.2 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ---------------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | | 404 | `COMPETITOR_NOT_FOUND` | Competitor doesn't exist | # Get Competitor Source: https://docs.qwairy.co/developers/endpoints/competitors/get GET /api/v1/brands/{brandId}/competitors/{competitorId} Retrieve detailed competitor info with breakdowns by provider and topic Returns detailed competitor information with breakdowns by provider and topic. ### Path Parameters The unique identifier of the brand The unique identifier of the competitor ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). ### Example Response ```json theme={null} { "success": true, "competitor": { "id": "cmp1", "name": "My Brand", "domain": "mybrand.com", "relationship": "SELF", "totalMentions": 104, "shareOfVoice": 8.13, "avgPosition": 2.1, "avgSentiment": 78.1, "byProvider": [ { "provider": "ChatGPT", "mentions": 62, "avgPosition": 2.0 }, { "provider": "Perplexity", "mentions": 42, "avgPosition": 2.3 } ], "byTopic": [ { "topic": "Product Reviews", "mentions": 45 }, { "topic": "Comparisons", "mentions": 38 } ] } } ``` # List Competitors Source: https://docs.qwairy.co/developers/endpoints/competitors/list GET /api/v1/brands/{brandId}/competitors Track competitor mentions, share of voice, and positioning Monitor how your brand and competitors are mentioned in AI responses. Competitors have three relationship types: `SELF` (your brand), `DIRECT` (direct competitors), and `INDIRECT` (indirect competitors). The list and evolution endpoints return only `SELF` and `DIRECT` by default. Share of Voice is calculated using only `SELF` and `DIRECT` mentions. See [Entities](/developers/entities#competitor) for the complete Competitor object structure. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by prompt type: `TOFU`, `MOFU`, `BOFU` Filter by relationship type. Supports comma-separated multi-select: `SELF`, `DIRECT`, `INDIRECT`. Default: `SELF,DIRECT`. Maximum number of results to return (max: 100) Number of results to skip for pagination Field to sort by: `mentions`, `position`, `sentiment`, `shareOfVoice`, `name` Sort order: `asc` or `desc` ### Response Returns competitors with relationship `SELF` (your brand) and `DIRECT` (direct competitors). `INDIRECT` competitors are not included by default. Indicates if the request was successful Total number of competitors matching filters Number of competitors in this response Maximum items per page Number of items skipped Competitor ID Competitor name Competitor domain `SELF` (your brand) or `DIRECT` (competitor) Total mentions count Share of voice (%) Average position in responses Average sentiment score (0-100) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/competitors?limit=10&offset=0" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 10, "count": 2, "limit": 10, "offset": 0 }, "competitors": [ { "id": "cmp1", "name": "My Brand", "domain": "mybrand.com", "relationship": "SELF", "totalMentions": 104, "shareOfVoice": 8.13, "avgPosition": 2.1, "avgSentiment": 78.1 }, { "id": "cmp2", "name": "Competitor A", "domain": "competitor-a.com", "relationship": "DIRECT", "totalMentions": 111, "shareOfVoice": 8.73, "avgPosition": 1.8, "avgSentiment": 75.2 } ] } ``` # Get Content Source: https://docs.qwairy.co/developers/endpoints/content/get GET /api/v1/brands/{brandId}/content/{articleId} Retrieve a full Content Studio article including the markdown body Retrieve a single Content Studio article with its full markdown `content`, outline, metadata and sources. This is the endpoint to call from a CMS automation once you have an article ID from [List Content](/developers/endpoints/content/list). Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand The unique identifier of the article ### Response Indicates if the request was successful `id`, `name`, `domain` Article ID Article title URL slug or `null` Short description or `null` Full article body in **Markdown** Article type `DRAFT`, `GENERATING`, `LIVE` or `ARCHIVED` Target word count used for generation Actual word count Generation guidelines or `null` Whether a TL;DR was requested Whether an FAQ was requested SEO/initialization metadata (JSON) or `null` Structured outline with `sections` or `null` Published URL (if set by you) or `null` Publish timestamp (ISO 8601) or `null` Content generation timestamp (ISO 8601) or `null` Creation timestamp (ISO 8601) Last update timestamp (ISO 8601) Sources used for the article. Each object has `id`, `title`, `url`, `usageType`, `order`. A `404 RESOURCE_NOT_FOUND` is returned if the article does not exist or does not belong to the brand. ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/content/art_abc123" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com" }, "article": { "id": "art_abc123", "title": "How to choose the right solution in 2026", "slug": "how-to-choose-the-right-solution-2026", "description": "A practical buyer's guide.", "content": "# How to choose the right solution in 2026\n\n## Introduction\n\n...", "articleType": "HOW_TO", "status": "LIVE", "targetLength": 1500, "wordCount": 1480, "guidelines": null, "includeTldr": true, "includeFaq": false, "metaData": { "seo": { "metaTitle": "How to choose the right solution (2026 guide)", "metaDescription": "A practical buyer's guide for 2026." } }, "outline": { "sections": [ { "id": "s1", "level": 2, "title": "Introduction", "order": 0 } ], "generatedAt": "2026-05-08T14:20:00.000Z" }, "finalUrl": "https://mybrand.com/blog/how-to-choose", "publishedAt": "2026-05-10T09:00:00.000Z", "generatedAt": "2026-05-08T14:22:00.000Z", "createdAt": "2026-05-08T13:50:00.000Z", "updatedAt": "2026-05-10T09:00:00.000Z", "sources": [ { "id": "src_1", "title": "Industry benchmark report", "url": "https://example.com/benchmark", "usageType": "AUTO", "order": 0 } ] } } ``` # List Content Source: https://docs.qwairy.co/developers/endpoints/content/list GET /api/v1/brands/{brandId}/content List Content Studio articles for syndication to an external CMS Content Studio articles are AI-generated, brand-aware pieces of content. This read-only endpoint lists articles so you can pull finished content into an external CMS (for example via an automation workflow). The list returns metadata only — fetch the [article detail](/developers/endpoints/content/get) for the full markdown body. Defaults to `status=LIVE` so you only get published-ready articles. Pass `status` explicitly to list drafts or archived items. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Filter by status: `DRAFT`, `GENERATING`, `LIVE`, `ARCHIVED` Filter by type: `INFORMATIONAL`, `COMPARISON`, `HOW_TO`, `LISTICLE`, `REVIEW`, `CASE_STUDY`, `NEWS`, `OPINION`, `OTHER` Maximum number of articles to return (max: 100) Pagination offset Field to sort by: `createdAt`, `updatedAt`, `publishedAt`, `title`, `wordCount` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of articles matching filters Number of articles in this response Maximum items per page Number of items skipped Article ID (use with the detail endpoint) Article title URL slug or `null` Short description or `null` Article type `DRAFT`, `GENERATING`, `LIVE` or `ARCHIVED` Word count of the generated content Published URL (if set by you) or `null` Publish timestamp (ISO 8601) or `null` Content generation timestamp (ISO 8601) or `null` Creation timestamp (ISO 8601) Last update timestamp (ISO 8601) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/content?status=LIVE&limit=20" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 12, "count": 1, "limit": 20, "offset": 0 }, "articles": [ { "id": "art_abc123", "title": "How to choose the right solution in 2026", "slug": "how-to-choose-the-right-solution-2026", "description": "A practical buyer's guide.", "articleType": "HOW_TO", "status": "LIVE", "wordCount": 1480, "finalUrl": "https://mybrand.com/blog/how-to-choose", "publishedAt": "2026-05-10T09:00:00.000Z", "generatedAt": "2026-05-08T14:22:00.000Z", "createdAt": "2026-05-08T13:50:00.000Z", "updatedAt": "2026-05-10T09:00:00.000Z" } ] } ``` # Topic Evolution Source: https://docs.qwairy.co/developers/endpoints/keywords/evolution GET /api/v1/brands/{brandId}/keywords/{keywordId}/evolution Daily time-series metrics for a specific topic Returns daily time-series metrics for a specific topic (keyword), including mention rate, share of voice, sentiment, and source citation rate. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand The unique identifier of the topic ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by prompt type: `TOFU`, `MOFU`, `BOFU` ### Response Indicates if the request was successful Topic ID Topic text Date (YYYY-MM-DD format) Number of AI responses analyzed Percentage of responses mentioning brand (0-100) Brand mentions vs competitors (0-100) Average sentiment score Percentage of responses citing brand sources (0-100) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/keywords/kw1/evolution?period=30" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "keyword": { "id": "kw1", "text": "best CRM software" }, "evolution": [ { "date": "2026-02-15", "answers": 42, "mentionRate": 65.2, "shareOfVoice": 48.3, "avgSentiment": 72.1, "sourceRate": 55.0 }, { "date": "2026-02-16", "answers": 38, "mentionRate": 60.5, "shareOfVoice": 45.1, "avgSentiment": 74.2, "sourceRate": 52.6 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ------------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | | 404 | `KEYWORD_NOT_FOUND` | Topic doesn't exist or not accessible | # List Topics Source: https://docs.qwairy.co/developers/endpoints/keywords/list GET /api/v1/brands/{brandId}/keywords Analyze performance by topic (keyword) Track how your brand performs across different topics. Topics (also called keywords) are the semantic themes that organize your monitoring prompts. See [Entities](/developers/entities#keyword) for the complete Topic object structure. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Maximum number of topics to return (max: 100) Number of results to skip for pagination Field to sort by: `prompts`, `answers`, `mentionRate`, `shareOfVoice`, `sentiment`, `keyword` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of topics matching filters Number of topics in this response Maximum items per page Number of items skipped Topic ID Topic text Number of prompts with this topic Total AI responses analyzed Percentage of responses mentioning brand (0-100) Brand mentions vs competitors (0-100) Average sentiment score (0-100) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/keywords?limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 25, "count": 2, "limit": 10, "offset": 0 }, "keywords": [ { "id": "kw1", "keyword": "best CRM software", "totalPrompts": 15, "totalAnswers": 120, "brandMentionRate": 45.5, "shareOfVoice": 32.1, "avgSentiment": 78.3 }, { "id": "kw2", "keyword": "enterprise solutions", "totalPrompts": 8, "totalAnswers": 64, "brandMentionRate": 28.1, "shareOfVoice": 18.5, "avgSentiment": 72.0 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Local Businesses Source: https://docs.qwairy.co/developers/endpoints/local GET /api/v1/brands/{brandId}/local Local business recommendations from AI responses Access local business recommendations extracted from AI responses, primarily from Google AI Overview local results. Track which businesses AI platforms recommend for location-based queries. ## List Local Insights Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/local ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by business category (case-insensitive partial match) Maximum number of businesses to return (max: 100) Number of results to skip for pagination Field to sort by: `createdAt`, `rating`, `reviewCount`, `position`, `businessName` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of businesses matching filters Number of businesses in this response Maximum items per page Number of items skipped Local insight ID Business name Business category Business address Business website URL Position in the list (1 = top result) Business rating (0-5) Number of reviews Original prompt that triggered this result ISO 8601 timestamp ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/local?category=restaurant&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 42, "count": 2, "limit": 10, "offset": 0 }, "local": [ { "id": "loc1", "businessName": "La Maison Restaurant", "category": "French Restaurant", "address": "123 Main Street, Paris", "websiteUrl": "https://lamaison.fr", "position": 1, "rating": 4.8, "reviewCount": 2500, "prompt": "Best French restaurants in Paris", "createdAt": "2024-12-15T10:30:00Z" }, { "id": "loc2", "businessName": "Café Central", "category": "Café", "address": "45 Avenue Montaigne, Paris", "websiteUrl": "https://cafecentral.fr", "position": 2, "rating": 4.5, "reviewCount": 1800, "prompt": "Best French restaurants in Paris", "createdAt": "2024-12-15T10:30:00Z" } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Get GEO Matrix Source: https://docs.qwairy.co/developers/endpoints/matrix GET /api/v1/brands/{brandId}/matrix Get the GEO visibility matrix for your brand across providers and dimensions Get the GEO visibility matrix: rows (topics, tags, prompts, or funnel stages) crossed with columns (AI providers), showing composite scores, sub-scores, rankings, citations, and top competitors per cell. This endpoint returns a cross-tabulation of your brand's visibility. See [Performance](/developers/endpoints/performance) for aggregated metrics and [Competitor](/developers/entities#competitor) for entity structures used in `topCompetitors`. ## Get Matrix Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/matrix ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Group rows by: `topics` (default), `tags`, `prompts`, or `funnel` Number of days to include. Default: 30 Start date (ISO 8601 format). Overrides `period` if provided with `endDate` End date (ISO 8601 format). Overrides `period` if provided with `startDate` Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID (keywordId). Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Max rows to return. Default: 50, max: 100 ### Granularity Options | Value | Groups rows by | | --------- | ------------------------------ | | `topics` | Keywords/topics (default) | | `tags` | Tags applied to prompts | | `prompts` | Individual monitored questions | | `funnel` | Funnel stages (TOFU/MOFU/BOFU) | ### Response Indicates if the request was successful Brand ID Brand name The granularity used for this matrix (`topics`, `tags`, `prompts`, or `funnel`) List of AI providers used as columns. Each entry is an object with an `id` and a `name`. Provider ID (e.g., `chatgpt`, `perplexity`, `claude`) Display name of the provider Matrix rows, one per topic/tag/prompt/funnel stage Row identifier (topic ID, tag ID, prompt ID, or funnel stage) Display name of the row Number of distinct prompts in this row Row-level brand mention rate (%) across all providers Row-level coverage: how many responses in this row mention your brand. Reported per row, not per cell. Responses in this row that mention your brand Total responses in this row Coverage percentage (0-100) One cell per provider Provider ID Composite GEO score (0-100) Brand mention rate (%) Share of voice (%) Source citation rate (%) Average sentiment (0-100) Rank and total (e.g., `{"rank": 2, "total": 8}`) Rank and total Rank and total Rank and total Number of source citations Best (minimum) position your brand reaches across this cell's responses (lower is better; `null` if not mentioned) Name of the leading competitor in this cell (`null` if none) Top competitors in this cell, each with `name`, `mentions`, `avgPosition`, and `relationship` Top cited sources in this cell Per-provider average metrics across all returned rows. Provider ID Average composite GEO score for this provider (0-100) Average mention rate for this provider (%) Average share of voice for this provider (%) Average citation rate for this provider (%) Average sentiment for this provider (0-100) Total number of rows Average mention rate across all returned rows (%) Highest mention-rate row, or `null`. Shape: `{ rowId, rowName, mentionRate }`. Lowest non-zero mention-rate row below 50%, or `null`. Shape: `{ rowId, rowName, mentionRate }`. ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/matrix?granularity=topics&period=30&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand" }, "granularity": "topics", "providers": [ { "id": "chatgpt", "name": "ChatGPT" }, { "id": "perplexity", "name": "Perplexity" }, { "id": "claude", "name": "Claude" } ], "rows": [ { "id": "topic1", "name": "Product Reviews", "promptCount": 12, "mentionRate": 52.4, "coverage": { "count": 38, "total": 60, "percentage": 63.0 }, "cells": [ { "provider": "chatgpt", "score": 72, "subScores": { "mentionRate": 65.0, "shareOfVoice": 18.5, "citationRate": 30.2, "sentiment": 81.0 }, "ranking": { "mentionRate": { "rank": 2, "total": 8 }, "shareOfVoice": { "rank": 3, "total": 8 }, "citationRate": { "rank": 1, "total": 8 }, "sentiment": { "rank": 1, "total": 8 } }, "citations": 12, "position": 2, "topCompetitor": "Competitor A", "topCompetitors": [ { "name": "Competitor A", "mentions": 9, "avgPosition": 2, "relationship": "DIRECT" }, { "name": "Competitor B", "mentions": 6, "avgPosition": 3, "relationship": "DIRECT" } ], "topSources": [ { "domain": "industry-news.com", "citations": 8 } ] }, { "provider": "perplexity", "score": 58, "subScores": { "mentionRate": 50.0, "shareOfVoice": 12.3, "citationRate": 42.1, "sentiment": 76.0 }, "ranking": { "mentionRate": { "rank": 3, "total": 7 }, "shareOfVoice": { "rank": 4, "total": 7 }, "citationRate": { "rank": 2, "total": 7 }, "sentiment": { "rank": 2, "total": 7 } }, "citations": 18, "position": 3, "topCompetitor": "Competitor B", "topCompetitors": [ { "name": "Competitor B", "mentions": 11, "avgPosition": 2, "relationship": "DIRECT" }, { "name": "Competitor A", "mentions": 7, "avgPosition": 4, "relationship": "DIRECT" } ], "topSources": [ { "domain": "tech-review.com", "citations": 14 } ] }, { "provider": "claude", "score": 45, "subScores": { "mentionRate": 35.0, "shareOfVoice": 9.8, "citationRate": 15.0, "sentiment": 72.0 }, "ranking": { "mentionRate": { "rank": 5, "total": 6 }, "shareOfVoice": { "rank": 4, "total": 6 }, "citationRate": { "rank": 3, "total": 6 }, "sentiment": { "rank": 3, "total": 6 } }, "citations": 4, "position": 4, "topCompetitor": "Competitor A", "topCompetitors": [ { "name": "Competitor A", "mentions": 5, "avgPosition": 3, "relationship": "DIRECT" } ], "topSources": [ { "domain": "mybrand.com", "citations": 3 } ] } ] } ], "providerAverages": [ { "provider": "chatgpt", "score": 72, "mentionRate": 65.0, "shareOfVoice": 18.5, "citationRate": 30.2, "sentiment": 81.0 }, { "provider": "perplexity", "score": 58, "mentionRate": 50.0, "shareOfVoice": 12.3, "citationRate": 42.1, "sentiment": 76.0 }, { "provider": "claude", "score": 45, "mentionRate": 35.0, "shareOfVoice": 9.8, "citationRate": 15.0, "sentiment": 72.0 } ], "summary": { "totalRows": 12, "averageMentionRate": 50, "topPerforming": { "rowId": "topic1", "rowName": "Product Reviews", "mentionRate": 52.4 }, "needsAttention": { "rowId": "topic7", "rowName": "Pricing", "mentionRate": 31.0 } } } ``` ### Error Responses | Status | Code | Description | | ------ | ------------------- | --------------------------------------------------- | | 400 | `INVALID_PARAMETER` | Invalid query parameter (e.g., unknown granularity) | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Get Perception Snapshot Source: https://docs.qwairy.co/developers/endpoints/perception/get GET /api/v1/brands/{brandId}/perception/{snapshotId} Full detail of a single perception snapshot: scores, attribute alignment and SWOT Retrieve the full detail of a single completed perception snapshot: the four scores, the attribute alignment breakdown and the SWOT insights. Get a `snapshotId` from the [Brand Perception](/developers/endpoints/perception/list) endpoint's `history` array. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand The unique identifier of the perception snapshot ### Response Indicates if the request was successful `id`, `name`, `domain` Snapshot ID Month (1-12) Year `sentiment`, `alignment`, `consistency`, `factualAlignment` (0-100 or `null`) Brand attributes evaluated by AI. Each object has `text` (string) and `status` (`aligned`, `partial`, `missing`). Counts: `aligned`, `partial`, `missing`, `total`. SWOT arrays of strings: `strengths`, `weaknesses`, `opportunities`, `threats`. AI providers included in the analysis Creation timestamp (ISO 8601) Completion timestamp (ISO 8601) or `null` A `404 RESOURCE_NOT_FOUND` is returned if the snapshot does not exist, is not `COMPLETED`, or does not belong to the brand. ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/perception/snap_2026_05" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com" }, "snapshot": { "snapshotId": "snap_2026_05", "month": 5, "year": 2026, "scores": { "sentiment": 78, "alignment": 65, "consistency": 82, "factualAlignment": 71 }, "attributes": [ { "text": "Premium positioning", "status": "aligned" }, { "text": "Sustainable materials", "status": "partial" }, { "text": "Made in France", "status": "missing" } ], "attributeMetrics": { "aligned": 1, "partial": 1, "missing": 1, "total": 3 }, "insights": { "strengths": ["Strong brand recognition in core market"], "weaknesses": ["Limited awareness of product range"], "opportunities": ["Growing demand in adjacent category"], "threats": ["Aggressive competitor messaging"] }, "providers": ["chatgpt", "perplexity"], "createdAt": "2026-05-01T03:00:00.000Z", "completedAt": "2026-05-03T04:12:00.000Z" } } ``` # Brand Perception Source: https://docs.qwairy.co/developers/endpoints/perception/list GET /api/v1/brands/{brandId}/perception Monthly perception scores: sentiment, alignment, consistency and factual alignment Brand Perception tracks how AI models describe your brand over time. Each monthly snapshot carries four scores (0-100): **sentiment**, **alignment** (match with your positioning), **consistency** (agreement across AI providers) and **factual alignment** (accuracy against your known brand facts). This endpoint returns the latest snapshot, the previous one, month-over-month trends, averages and the full history — the same data shown on the dashboard Perception tab. Perception snapshots are generated monthly. There is no day-based `period` filter — use `months` to control the history depth. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of months of history to include (max: 24). Only `COMPLETED` snapshots are returned. ### Response Indicates if the request was successful Brand ID Brand name Brand domain Latest completed snapshot, or `null` if none. Contains `snapshotId`, `month`, `year`, `completedAt`, and a `scores` object with `sentiment`, `alignment`, `consistency`, `factualAlignment` (each `0-100` or `null`). Previous completed snapshot in the same shape as `current`, or `null`. Difference (current − previous) for each score: `sentiment`, `alignment`, `consistency`, `factualAlignment`. A value is `null` when either side is missing. Average of each score across the returned history (`null` when no data points). Snapshot ID (use with the detail endpoint) Month (1-12) Year `MM/YYYY` label `sentiment`, `alignment`, `consistency`, `factualAlignment` Completion timestamp (ISO 8601) or `null` `months` (requested window) and `dataPoints` (number of snapshots returned). Estimated date of the next monthly snapshot (ISO 8601). ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/perception?months=6" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com" }, "current": { "snapshotId": "snap_2026_05", "month": 5, "year": 2026, "scores": { "sentiment": 78, "alignment": 65, "consistency": 82, "factualAlignment": 71 }, "completedAt": "2026-05-03T04:12:00.000Z" }, "previous": { "snapshotId": "snap_2026_04", "month": 4, "year": 2026, "scores": { "sentiment": 74, "alignment": 61, "consistency": 80, "factualAlignment": 69 }, "completedAt": "2026-04-02T04:09:00.000Z" }, "trends": { "sentiment": 4, "alignment": 4, "consistency": 2, "factualAlignment": 2 }, "averages": { "sentiment": 75.5, "alignment": 62.0, "consistency": 80.3, "factualAlignment": 69.8 }, "history": [ { "snapshotId": "snap_2026_04", "month": 4, "year": 2026, "label": "04/2026", "scores": { "sentiment": 74, "alignment": 61, "consistency": 80, "factualAlignment": 69 }, "completedAt": "2026-04-02T04:09:00.000Z" }, { "snapshotId": "snap_2026_05", "month": 5, "year": 2026, "label": "05/2026", "scores": { "sentiment": 78, "alignment": 65, "consistency": 82, "factualAlignment": 71 }, "completedAt": "2026-05-03T04:12:00.000Z" } ], "meta": { "months": 6, "dataPoints": 2 }, "nextAnalysisDate": "2026-06-01T00:00:00.000Z" } ``` # Get Performance Snapshot Source: https://docs.qwairy.co/developers/endpoints/performance GET /api/v1/brands/{brandId}/performance Get performance snapshots and GEO scores for your brand Get a comprehensive overview of your brand's visibility in AI-generated responses. Includes mention rates, source citations, sentiment scores, share of voice, and breakdowns by topic and competitor. This endpoint returns aggregated metrics. See [Competitor](/developers/entities#competitor) and [Source](/developers/entities#source) for entity structures used in `topCompetitors` and `topSources`. ### Key Formulas Understanding how metrics are calculated is essential for reproducing results: | Metric | Formula | Details | | ------------------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Mention Rate** | `mentionCount / mentionTotal × 100` | `mentionCount` = responses where your brand (SELF) is mentioned. `mentionTotal` = responses with **at least one SELF or DIRECT competitor mention** (not total responses). | | **Coverage** | `mentionCount / responsesTotal × 100` | Percentage of **all** monitored responses mentioning your brand. Unlike Mention Rate, uses total responses as denominator. | | **Source Rate** | `sourceCount / sourceTotal × 100` | `sourceCount` = responses citing your domain (SELF). `sourceTotal` = responses with **at least one SELF or DIRECT source citation** (not total responses). | | **Share of Voice** | `sovSelf / sovTotal × 100` | `sovSelf` = distinct SELF mention count. `sovTotal` = distinct SELF + DIRECT mention count. **INDIRECT mentions are excluded.** | **Common mistake:** Using total responses as the denominator for Mention Rate, or including INDIRECT competitor mentions in the Share of Voice denominator. The API returns `sovSelf` and `sovTotal` so you can verify the calculation. ## Get Performance Snapshot Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/performance ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include in the snapshot. If not specified, returns all data. Start date (ISO 8601 format). Overrides `period` if provided with `endDate` End date (ISO 8601 format). Overrides `period` if provided with `startDate` Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID (keywordId). Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by prompt type: `TOFU`, `MOFU`, `BOFU` ### Response Indicates if the request was successful Brand ID Brand name Primary domain Start date (ISO 8601) End date (ISO 8601) Total prompts analyzed Number of AI providers List of provider IDs (e.g., `chatgpt`, `perplexity`) Total AI responses generated Responses containing brand mentions Responses citing sources Brand mention rate (%) Number of responses where your brand (SELF) is mentioned Number of responses containing **at least one SELF or DIRECT competitor mention**. This is the denominator for `mentionRate` — it is NOT the total number of responses. INDIRECT mentions are excluded. Brand coverage (%). Percentage of all monitored responses where your brand is mentioned. Formula: `mentionCount / responsesTotal × 100`. Source citation rate (%) Number of source citations Number of responses containing **at least one SELF or DIRECT source citation**. This is the denominator for `sourceRate` — it is NOT the total number of responses. INDIRECT sources are excluded. Number of unique URLs from your domain (SELF) cited as sources across all AI responses. Average sentiment score (0-100) Share of voice (%). Formula: SELF mentions / (SELF + DIRECT mentions) × 100. INDIRECT mentions are excluded. Top competitors by mention count Top sources by citation count Topic ID Topic name Composite score (0-100). Formula: `mentionRate × 0.5 + sourceRate × 0.3 + shareOfVoice × 0.2`. Mention rate for this topic (%). Formula: `selfMentionAnswers / responsesWithMentions × 100` where `responsesWithMentions` counts answers in this topic with at least one SELF or DIRECT competitor mention. Same denominator semantic as the top-level `scores.mentionRate`. Source citation rate for this topic (%). Formula: `selfSourceAnswers / responsesWithSources × 100` where `responsesWithSources` counts answers in this topic with at least one SELF or DIRECT source citation. Same denominator semantic as the top-level `scores.sourceRate` (INDIRECT excluded). Share of voice for this topic (%). Formula: SELF mentions in topic / (SELF + DIRECT mentions in topic) × 100. Average sentiment score for brand mentions in this topic (null if no mentions) Number of distinct prompts in this topic Tag ID Tag name Composite score (0-100). Formula: `mentionRate × 0.5 + sourceRate × 0.3 + shareOfVoice × 0.2`. Mention rate for this tag (%). Formula: `selfMentionAnswers / responsesWithMentions × 100` where `responsesWithMentions` counts answers in this tag with at least one SELF or DIRECT competitor mention. Same denominator semantic as the top-level `scores.mentionRate`. Source citation rate for this tag (%). Formula: `selfSourceAnswers / responsesWithSources × 100` where `responsesWithSources` counts answers in this tag with at least one SELF or DIRECT source citation. Same denominator semantic as the top-level `scores.sourceRate` (INDIRECT excluded). Share of voice for this tag (%). Formula: SELF mentions in tag / (SELF + DIRECT mentions in tag) × 100. Average sentiment score for brand mentions in this tag (null if no mentions) Number of distinct prompts in this tag ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/performance?period=30" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com" }, "period": { "start": "2024-12-01", "end": "2024-12-31" }, "methodology": { "promptsCount": 156, "providersCount": 2, "providers": ["chatgpt", "perplexity"], "responsesTotal": 312, "responsesWithMentions": 230, "responsesWithSources": 184 }, "scores": { "mentionRate": 45.2, "mentionCount": 104, "mentionTotal": 230, "coverage": 33.33, "sourceRate": 23.9, "sourceCount": 44, "sourceTotal": 184, "sourcePages": 27, "sentiment": 78.1, "shareOfVoice": 8.13 }, "topCompetitors": [ { "id": "cmp1", "name": "Competitor A", "relationship": "DIRECT", "mentions": 111, "avgPosition": 2.3, "avgSentiment": 75.2 } ], "topSources": [ { "id": "src1", "domain": "industry-news.com", "mentions": 102, "avgPosition": 3.1, "isSelf": false } ], "byTopic": [ { "id": "topic1", "topic": "Product Reviews", "score": 68, "mentionRate": 67.50, "sourceRate": 28.00, "shareOfVoice": 9.44, "avgSentiment": 76.3, "promptsCount": 12 } ], "byTag": [ { "id": "tag1", "name": "comparison", "score": 72, "mentionRate": 55.00, "sourceRate": 30.00, "shareOfVoice": 10.25, "avgSentiment": 81.5, "promptsCount": 8 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ------------------- | ------------------------------------- | | 400 | `INVALID_PARAMETER` | Invalid query parameter | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Prompt Answers Source: https://docs.qwairy.co/developers/endpoints/prompts/answers GET /api/v1/brands/{brandId}/prompts/{promptId}/answers Retrieve all AI-generated responses for a specific prompt Retrieve all AI-generated responses for a specific prompt, including detected brand mentions and cited sources. ### Path Parameters The unique identifier of the brand The unique identifier of the prompt ### Query Parameters Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/prompts/q1/answers" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "prompt": { "id": "q1", "text": "What are the best products in this category?" }, "total": 2, "answers": [ { "id": "ans1", "provider": "ChatGPT", "model": "GPT-4o", "text": "Based on recent reviews and comparisons...", "hasSelfMention": true, "selfMentionPosition": 3, "hasSelfSource": false, "competitorsMentioned": ["My Brand", "Competitor A", "Competitor B"], "sourcesCited": ["industry-news.com", "expert-reviews.com"], "sentiment": 82, "createdAt": "2024-12-19T10:00:00.000Z" }, { "id": "ans2", "provider": "Perplexity", "model": "Sonar Large", "text": "According to expert reviews and user feedback...", "hasSelfMention": true, "selfMentionPosition": 2, "hasSelfSource": true, "competitorsMentioned": ["My Brand", "Competitor C", "Competitor D"], "sourcesCited": ["mybrand.com", "consumer-reports.com"], "sentiment": 78, "createdAt": "2024-12-19T10:05:00.000Z" } ] } ``` # Prompt Evolution Source: https://docs.qwairy.co/developers/endpoints/prompts/evolution GET /api/v1/brands/{brandId}/prompts/{promptId}/evolution Track how a prompt's metrics change over time Track how a prompt's metrics change over time. Returns daily time-series data for mentions, sources, and sentiment. ### Path Parameters The unique identifier of the brand The unique identifier of the prompt ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/prompts/q1/evolution?period=7" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "prompt": { "id": "q1", "text": "What are the best products in this category?" }, "evolution": [ { "date": "2024-12-01", "answers": 5, "mentionRate": 80.0, "sourceRate": 60.0, "avgSentiment": 78.5 }, { "date": "2024-12-02", "answers": 8, "mentionRate": 75.0, "sourceRate": 62.5, "avgSentiment": 80.1 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ------------------- | ------------------------------------- | | 400 | `INVALID_PARAMETER` | Invalid query parameter | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | | 404 | `PROMPT_NOT_FOUND` | Prompt doesn't exist | # Get Prompt Source: https://docs.qwairy.co/developers/endpoints/prompts/get GET /api/v1/brands/{brandId}/prompts/{promptId} Retrieve detailed information about a specific prompt Retrieve detailed information about a single prompt, including aggregated metrics across all its answers. ### Path Parameters The unique identifier of the brand The unique identifier of the prompt ### Query Parameters Set to `details` to enrich the prompt with aggregated competitors, sources, brand mentions, and source stability metrics. When omitted, returns the base fields only. ### Response Returns detailed prompt information with aggregated metrics. ### Example Response ```json theme={null} { "success": true, "prompt": { "id": "q1", "text": "What are the best products in this category?", "topic": { "id": "topic1", "name": "Product Reviews" }, "type": "TOFU", "tags": [ { "id": "tag1", "name": "reviews" }, { "id": "tag2", "name": "comparison" } ], "answersCount": 2, "mentionRate": 50.0, "sourceRate": 25.0, "avgSentiment": 78.5, "createdAt": "2024-12-01T10:00:00.000Z", "lastGeneratedAt": "2024-12-19T10:00:00.000Z" } } ``` ### Example with `include=details` ```json theme={null} { "success": true, "prompt": { "id": "q1", "text": "What are the best products in this category?", "topic": { "id": "topic1", "name": "Product Reviews" }, "type": "TOFU", "tags": [ { "id": "tag1", "name": "reviews" }, { "id": "tag2", "name": "comparison" } ], "answersCount": 2, "mentionRate": 50.0, "sourceRate": 25.0, "avgSentiment": 78.5, "createdAt": "2024-12-01T10:00:00.000Z", "lastGeneratedAt": "2024-12-19T10:00:00.000Z", "competitors": [ { "name": "My Brand", "mentions": 3, "relationship": "SELF" }, { "name": "Competitor A", "mentions": 8, "relationship": "DIRECT" } ], "competitorsCount": 2, "competitorMentionsTotal": 11, "brandMentioned": true, "brandMentionCount": 3, "sources": [ { "domain": "mybrand.com", "citations": 2, "isSelf": true }, { "domain": "expert-reviews.com", "citations": 4, "isSelf": false } ], "sourcesCount": 2, "sourceCitationsTotal": 6, "sourceStability": 50.0 } } ``` # List Prompts Source: https://docs.qwairy.co/developers/endpoints/prompts/list GET /api/v1/brands/{brandId}/prompts Get paginated prompts with search and filtering options Prompts are the questions Qwairy monitors across AI platforms. Each prompt is categorized by funnel stage (TOFU/MOFU/BOFU), tagged for organization, and linked to a topic. Track which prompts generate mentions and source citations for your brand. See [Entities](/developers/entities#prompt) for the complete Prompt object structure, and [Topic](/developers/entities#topic) / [Tag](/developers/entities#tag) for classification objects. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. Returns prompts that received at least one answer within the window. If not specified, returns all data. Start date (ISO 8601, e.g. `2026-03-05`). Returns prompts with at least one answer on or after this date. End date (ISO 8601, e.g. `2026-03-31`). Returns prompts with at least one answer on or before this date. When a date range is provided, both the prompt list and the per-prompt `include=details` aggregations (`answersCount`, `competitors[].mentions`, `brandMentionCount`, `sources[].citations`) are scoped to that window. Prompts that ran multiple times still appear if at least one of their runs falls inside the range. Filter by topic ID (keywordId). Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by prompt type: `TOFU`, `MOFU`, `BOFU` Maximum number of prompts to return (max: 100) Pagination offset Field to sort by: `lastGeneratedAt`, `answersCount`, `text`, `createdAt` Sort order: `asc` or `desc` Set to `details` to enrich each prompt with aggregated competitors, sources, brand mentions, and source stability metrics. When omitted, the response includes only the base fields listed below. ### Response Indicates if the request was successful Total number of prompts matching filters Number of prompts in this response Maximum items per page Number of items skipped Prompt ID Prompt text Associated topic name Prompt type (TOFU/MOFU/BOFU) List of tag names Number of generated answers Brand mention rate (%) Source citation rate (%) Last generation timestamp (ISO 8601) List of competitors mentioned across all answers for this prompt. Each object contains `name` (string), `mentions` (number), and `relationship` (`SELF`, `DIRECT`, `INDIRECT`). Number of unique competitors mentioned Total competitor mentions across all answers Whether your brand was mentioned in any answer Number of times your brand was mentioned List of source domains cited across all answers for this prompt. Each object contains `domain` (string), `citations` (number), and `isSelf` (boolean). Number of unique source domains cited Total source citations across all answers Percentage of sources present in all answers (0-100). Indicates how consistently sources are cited across different AI responses. Prompt creation timestamp (ISO 8601) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/prompts?type=TOFU&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 1, "limit": 10, "offset": 0 }, "prompts": [ { "id": "q1", "text": "What are the best products in this category?", "topic": "Product Reviews", "type": "TOFU", "tags": ["reviews", "comparison"], "answersCount": 2, "mentionRate": 50.0, "sourceRate": 25.0, "lastGeneratedAt": "2024-12-19T10:00:00.000Z" } ] } ``` ### Example with `include=details` ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/prompts?include=details&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 1, "limit": 10, "offset": 0 }, "prompts": [ { "id": "q1", "text": "What are the best products in this category?", "topic": "Product Reviews", "type": "TOFU", "tags": ["reviews", "comparison"], "answersCount": 2, "mentionRate": 50.0, "sourceRate": 25.0, "lastGeneratedAt": "2024-12-19T10:00:00.000Z", "competitors": [ { "name": "My Brand", "mentions": 3, "relationship": "SELF" }, { "name": "Competitor A", "mentions": 8, "relationship": "DIRECT" }, { "name": "Competitor B", "mentions": 5, "relationship": "DIRECT" } ], "competitorsCount": 3, "competitorMentionsTotal": 16, "brandMentioned": true, "brandMentionCount": 3, "sources": [ { "domain": "mybrand.com", "citations": 2, "isSelf": true }, { "domain": "expert-reviews.com", "citations": 4, "isSelf": false }, { "domain": "industry-news.com", "citations": 3, "isSelf": false } ], "sourcesCount": 3, "sourceCitationsTotal": 9, "sourceStability": 33.33, "createdAt": "2024-12-01T10:00:00.000Z" } ] } ``` # Query Fan-Out Source: https://docs.qwairy.co/developers/endpoints/search GET /api/v1/brands/{brandId}/search Web search queries generated by AI (Query Fan Out) Access aggregated web search queries that AI providers generate when responding to prompts. Results are grouped by query text with brand presence, competitor presence, and priority metrics. Results are aggregated by query (case-insensitive, trimmed). Each row represents a unique search query across all responses and providers. ## List Search Insights Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/search ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Maximum number of queries to return (max: 100) Number of results to skip for pagination Field to sort by: `occurrences`, `priority`, `brandPresence`, `query`, `createdAt` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of unique queries matching filters Number of queries in this response Maximum items per page Number of items skipped The search query used by the AI Number of times this query appeared across all responses Priority level: `very-high` (competitors present, brand absent), `high` (both present), `medium` (brand only), `low` (neither) Number of responses where the brand's domain was cited as source Number of responses where direct competitors were mentioned List of direct competitor names mentioned Number of distinct prompts that triggered this query Number of distinct AI responses containing this query Number of distinct AI models that generated this query List of model IDs that generated this query Associated topics Associated tags First occurrence (ISO 8601 timestamp) Most recent occurrence (ISO 8601 timestamp) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/search?limit=10&sort=priority" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 142, "count": 2, "limit": 10, "offset": 0 }, "searches": [ { "query": "best CRM software 2024 enterprise features comparison", "occurrences": 12, "priority": "very-high", "brandPresence": 0, "competitorPresence": 8, "competitors": ["Salesforce", "HubSpot"], "uniquePrompts": 5, "uniqueAnswers": 12, "uniqueModels": 3, "models": ["gpt-4o", "claude-3-5-sonnet", "gemini-1.5-pro"], "topics": ["CRM", "Enterprise Software"], "tags": ["comparison"], "firstSeen": "2024-12-01T10:30:00.000Z", "lastSeen": "2024-12-15T10:30:00.000Z" }, { "query": "Acme CRM pricing plans 2024", "occurrences": 5, "priority": "medium", "brandPresence": 3, "competitorPresence": 0, "competitors": [], "uniquePrompts": 2, "uniqueAnswers": 5, "uniqueModels": 2, "models": ["gpt-4o", "gemini-1.5-pro"], "topics": ["Pricing"], "tags": [], "firstSeen": "2024-12-05T09:15:00.000Z", "lastSeen": "2024-12-14T09:15:00.000Z" } ] } ``` **Breaking change from previous version:** Results are now aggregated by unique query text. The previous `id` and `prompt` fields have been removed. Use `lastSeen` instead of the previous `createdAt` field for timestamp ordering. Search queries reveal valuable keyword and intent data. Use `priority` to identify high-value queries where competitors appear but your brand doesn't (`very-high`). ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Shopping Results Source: https://docs.qwairy.co/developers/endpoints/shopping GET /api/v1/brands/{brandId}/shopping Product recommendations from AI responses Access product recommendations extracted from AI responses, primarily from Google AI Overview shopping results. Track which products AI platforms recommend in your industry. ## List Shopping Insights Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/shopping ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Maximum number of products to return (max: 100) Number of results to skip for pagination Field to sort by: `createdAt`, `rating`, `reviews`, `productName` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of products matching filters Number of products in this response Maximum items per page Number of items skipped Shopping insight ID Product name Product description Product image URL Product rating (0-5) Number of reviews Purchasing options with prices and sellers Original prompt that triggered this result ISO 8601 timestamp ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/shopping?limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 85, "count": 2, "limit": 10, "offset": 0 }, "shopping": [ { "id": "shop1", "productName": "Acme Pro CRM Suite", "description": "Enterprise-grade CRM solution with AI features", "image": "https://example.com/product-image.jpg", "rating": 4.5, "reviews": 1250, "options": [ { "price": "$99/month", "currency": "USD", "websiteName": "Acme", "websiteUrl": "https://acme.com/pricing" } ], "prompt": "What is the best CRM software for enterprise?", "createdAt": "2024-12-15T10:30:00Z" }, { "id": "shop2", "productName": "DataFlow Analytics Pro", "description": "Real-time analytics platform", "image": null, "rating": 4.2, "reviews": 890, "options": [], "prompt": "Compare top analytics platforms", "createdAt": "2024-12-14T15:45:00Z" } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Social Signals Source: https://docs.qwairy.co/developers/endpoints/social GET /api/v1/brands/{brandId}/social Social platform citations from AI responses Track citations from social platforms (Reddit, YouTube, Twitter, forums) in AI responses. Understand which social content AI platforms reference when answering questions about your industry. ## List Social Insights Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/social ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by platform: `reddit`, `youtube`, `twitter`, `facebook`, `linkedin`, `other` Maximum number of citations to return (max: 100) Number of results to skip for pagination Field to sort by: `createdAt`, `position`, `upvotes`, `comments`, `url` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of citations matching filters Number of citations in this response Maximum items per page Number of items skipped Social insight ID Platform name (reddit, youtube, twitter, etc.) Community identifier (subreddit, channel, etc.) Community display name Full URL of the cited content Content title (if available) Domain name Position in source list Upvotes/likes (Reddit, when available) Comment count (when available) ISO 8601 timestamp ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/social?platform=reddit&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 2, "limit": 10, "offset": 0 }, "social": [ { "id": "soc1", "platform": "reddit", "communityId": "SaaS", "communityName": "r/SaaS", "url": "https://reddit.com/r/SaaS/comments/abc123/best_crm_tools", "title": "Best CRM tools for startups in 2024?", "domain": "reddit.com", "position": 2, "upvotes": 245, "commentCount": 89, "createdAt": "2024-12-15T10:30:00Z" }, { "id": "soc2", "platform": "youtube", "communityId": "TechReviewer", "communityName": "TechReviewer", "url": "https://youtube.com/@TechReviewer/crm-comparison", "title": "CRM Software Comparison 2024", "domain": "youtube.com", "position": 3, "upvotes": 0, "commentCount": 0, "createdAt": "2024-12-14T15:45:00Z" } ] } ``` Social signals like upvotes and comments are only available for platforms that expose this data (primarily Reddit). Other platforms may show 0 for these fields. ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Source Domain Evolution Source: https://docs.qwairy.co/developers/endpoints/source-domains/evolution GET /api/v1/brands/{brandId}/source-domains/{sourceId}/evolution Track how a source's citation metrics change over time Track how a source's citation metrics change over time. Returns daily time-series data for mentions and average position. ### Path Parameters The unique identifier of the brand The unique identifier of the source ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/source-domains/src1/evolution?period=7" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "source": { "id": "src1", "domain": "industry-news.com", "isSelf": false }, "evolution": [ { "date": "2024-12-01", "mentions": 5, "avgPosition": 3.1 }, { "date": "2024-12-02", "mentions": 8, "avgPosition": 2.9 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ------------------ | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | | 404 | `SOURCE_NOT_FOUND` | Source doesn't exist | # Get Source Domain Source: https://docs.qwairy.co/developers/endpoints/source-domains/get GET /api/v1/brands/{brandId}/source-domains/{sourceId} Retrieve detailed source info with breakdowns by provider and topic Returns detailed source information with breakdowns by provider and topic. ### Path Parameters The unique identifier of the brand The unique identifier of the source ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) ### Example Response ```json theme={null} { "success": true, "source": { "id": "src1", "domain": "industry-news.com", "type": "MEDIA", "status": "ACTIVE", "isSelf": false, "totalMentions": 102, "rate": 5.10, "avgPosition": 3.2, "byProvider": [ { "provider": "ChatGPT", "mentions": 60, "avgPosition": 3.1 }, { "provider": "Perplexity", "mentions": 42, "avgPosition": 3.4 } ], "byTopic": [ { "topic": "Product Reviews", "mentions": 55, "avgPosition": 2.8 }, { "topic": "Comparisons", "mentions": 30, "avgPosition": 3.5 } ] } } ``` # List Source Domains Source: https://docs.qwairy.co/developers/endpoints/source-domains/list GET /api/v1/brands/{brandId}/source-domains Monitor which domains AI platforms cite Track which websites AI platforms cite when answering questions about your industry. Identify opportunities to get your content cited, and monitor which sources influence AI responses in your space. See [Entities](/developers/entities#source) for the complete Source object structure. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by source type: `INSTITUTIONAL`, `COMMERCIAL`, `MEDIA`, `BLOG`, etc. Filter by topic ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by tag ID. Supports comma-separated multi-select (e.g., `id1,id2`). Maximum number of sources to return (max: 100) Number of results to skip for pagination Field to sort by: `mentions`, `position`, `rate`, `domain` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of sources matching filters Number of sources in this response Maximum items per page Number of items skipped Source ID Domain name Source type Whether this is your own domain Total citation count Share of Citations (%) - percentage of all citations from this source Average position in source lists ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/source-domains?limit=10&offset=0" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 45, "count": 2, "limit": 10, "offset": 0 }, "sources": [ { "id": "src1", "domain": "industry-news.com", "type": "MEDIA", "isSelf": false, "totalMentions": 102, "rate": 5.10, "avgPosition": 3.2 }, { "id": "src2", "domain": "mybrand.com", "type": "INSTITUTIONAL", "isSelf": true, "totalMentions": 45, "rate": 2.25, "avgPosition": 2.1 } ] } ``` # List Source URLs Source: https://docs.qwairy.co/developers/endpoints/source-urls GET /api/v1/brands/{brandId}/source-urls Track individual page citations Get granular URL-level citation data. While the Sources endpoint shows domain-level aggregates, this endpoint provides page-by-page citation details. ## List Source URLs Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/source-urls ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by specific domain Filter by self domains: `true` or `false` Filter by competitor domains: `true` or `false` Maximum number of URLs to return (max: 100) Number of results to skip for pagination Field to sort by: `mentions`, `position`, `shareOfVoice`, `url` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of URLs matching filters Number of URLs in this response Maximum items per page Number of items skipped Full URL of the cited page Page title (if available) Domain name Whether this is your domain Whether this is a direct competitor's domain Names of direct competitors associated with this URL Total citation count Average position in source lists Share of all citations (0-100) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/source-urls?isSelf=true&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 2, "limit": 10, "offset": 0 }, "sourceUrls": [ { "url": "https://mybrand.com/blog/best-practices", "title": "Best Practices Guide - MyBrand", "domain": "mybrand.com", "isSelf": true, "isCompetitor": false, "competitors": [], "totalMentions": 45, "avgPosition": 2.1, "shareOfVoice": 3.5 }, { "url": "https://competitor.com/blog/review", "title": "Product Review - Competitor", "domain": "competitor.com", "isSelf": false, "isCompetitor": true, "competitors": ["Competitor Inc"], "totalMentions": 32, "avgPosition": 3.4, "shareOfVoice": 2.1 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Sponsored Content Source: https://docs.qwairy.co/developers/endpoints/sponsored GET /api/v1/brands/{brandId}/sponsored Sponsored placements detected in AI responses Access sponsored content (ads) detected in AI responses, primarily from ChatGPT. Track which advertisers appear, what products they promote, and which prompts trigger these placements. ## List Sponsored Content Bearer token. Example: `Bearer qw-api-xxx` ```http theme={null} GET /api/v1/brands/{brandId}/sponsored ``` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt`). Filter by advertiser name (case-insensitive partial match) Maximum number of results to return (max: 100) Number of results to skip for pagination Field to sort by: `createdAt`, `advertiserName`, `productName`, `position` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of sponsored placements matching filters Number of items in this response Maximum items per page Number of items skipped Sponsored content record ID Name of the advertiser URL of the advertiser's logo Name of the advertised product Product description text Product image URL Position of the ad within the response (1 = first) AI provider model ID Relationship to your brand if advertiser is a known competitor: `SELF`, `DIRECT`, `INDIRECT`, or `null` Original prompt that triggered this sponsored placement ISO 8601 timestamp ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/sponsored?period=30&limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 24, "count": 2, "limit": 10, "offset": 0 }, "sponsored": [ { "id": "sp1", "advertiserName": "Expedia", "advertiserLogo": "https://bzrcdn.openai.com/expedia-logo.png", "productName": "Cheap Flights from Chicago, IL", "description": "Compare Cheap Flights from Chicago, Illinois. View Deals and Book Now on Expedia.", "imageUrl": null, "position": 1, "provider": "chatgpt-4o", "competitorRelationship": "DIRECT", "prompt": "What are the best flight deals from Chicago?", "createdAt": "2026-04-10T14:30:00Z" }, { "id": "sp2", "advertiserName": "Nordstrom", "advertiserLogo": "https://bzrcdn.openai.com/nordstrom-logo.png", "productName": "Spring Collection Sale", "description": "Shop the latest spring styles at Nordstrom. Free shipping and returns.", "imageUrl": null, "position": 1, "provider": "chatgpt-4o", "competitorRelationship": null, "prompt": "Best spring fashion deals 2026", "createdAt": "2026-04-09T09:15:00Z" } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Tag Evolution Source: https://docs.qwairy.co/developers/endpoints/tags/evolution GET /api/v1/brands/{brandId}/tags/{tagId}/evolution Daily time-series metrics for a specific tag Returns daily time-series metrics for a specific tag, including mention rate, share of voice, sentiment, and source citation rate. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand The unique identifier of the tag ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic (keyword) ID. Supports comma-separated multi-select (e.g., `id1,id2`). Filter by prompt type: `TOFU`, `MOFU`, `BOFU` ### Response Indicates if the request was successful Tag ID Tag name Date (YYYY-MM-DD format) Number of AI responses analyzed Percentage of responses mentioning brand (0-100) Brand mentions vs competitors (0-100) Average sentiment score Percentage of responses citing brand sources (0-100) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/tags/tag1/evolution?period=30" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "tag": { "id": "tag1", "name": "Product Comparisons" }, "evolution": [ { "date": "2026-02-15", "answers": 42, "mentionRate": 65.2, "shareOfVoice": 48.3, "avgSentiment": 72.1, "sourceRate": 55.0 }, { "date": "2026-02-16", "answers": 38, "mentionRate": 60.5, "shareOfVoice": 45.1, "avgSentiment": 74.2, "sourceRate": 52.6 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | | 404 | `TAG_NOT_FOUND` | Tag doesn't exist or not accessible | # List Tags Source: https://docs.qwairy.co/developers/endpoints/tags/list GET /api/v1/brands/{brandId}/tags Analyze performance by tag Track how your brand performs across different tags. Tags are custom labels you can apply to prompts for grouping and filtering. See [Entities](/developers/entities#tag) for the complete Tag object structure. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### Query Parameters Number of days to include. If not specified, returns all data. Start date (ISO 8601 format) End date (ISO 8601 format) Filter by AI provider. Supports comma-separated multi-select (e.g., `chatgpt,claude`). Filter by topic (keyword) ID. Supports comma-separated multi-select (e.g., `id1,id2`). Maximum number of tags to return (max: 100) Number of results to skip for pagination Field to sort by: `prompts`, `answers`, `mentionRate`, `shareOfVoice`, `sentiment`, `name` Sort order: `asc` or `desc` ### Response Indicates if the request was successful Total number of tags matching filters Number of tags in this response Maximum items per page Number of items skipped Tag ID Tag name Number of prompts with this tag Total AI responses analyzed Percentage of responses mentioning brand (0-100) Brand mentions vs competitors (0-100) Average sentiment score (0-100) ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/tags?limit=10" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "pagination": { "total": 12, "count": 2, "limit": 10, "offset": 0 }, "tags": [ { "id": "tag1", "name": "Product Comparisons", "totalPrompts": 25, "totalAnswers": 200, "brandMentionRate": 52.0, "shareOfVoice": 38.5, "avgSentiment": 81.2 }, { "id": "tag2", "name": "Pricing Questions", "totalPrompts": 18, "totalAnswers": 144, "brandMentionRate": 35.4, "shareOfVoice": 22.1, "avgSentiment": 68.5 } ] } ``` ### Error Responses | Status | Code | Description | | ------ | ----------------- | ------------------------------------- | | 401 | `INVALID_TOKEN` | Authentication failed | | 404 | `BRAND_NOT_FOUND` | Brand doesn't exist or not accessible | # Site Readiness Source: https://docs.qwairy.co/developers/endpoints/technical-analysis GET /api/v1/brands/{brandId}/technical-analysis AI Readiness Score and robots.txt / llms.txt / sitemap.xml analysis Returns the stored technical analysis for a brand - the robots.txt, llms.txt and sitemap.xml checks - plus the **AI Readiness Score** (0-100), the exact value shown on the dashboard Optimize > AI Readiness tab. This endpoint is read-only: it serves the last stored analysis, it does not trigger a fresh crawl. The analysis is generated the first time a brand's AI Readiness tab is opened in the app. If it has never been opened, `analyzed` is `false` and the analysis fields are `null`. Bearer token. Example: `Bearer qw-api-xxx` ### Path Parameters The unique identifier of the brand ### AI Readiness Score formula Out of 100: * robots.txt present: **+20** * crawlers allowed: **+round((allowed / total) × 40)** over GPTBot, ChatGPT-User, Claude-Web, ClaudeBot, Google-Extended, PerplexityBot. Crawlers are only parsed when robots.txt is present, so a missing robots.txt scores 0 here. * llms.txt present: **+20** * sitemap.xml present: **+20** A missing or invalid robots.txt therefore caps the score at 40. ### Response Indicates if the request was successful `id`, `name`, `domain` `false` when no analysis has been generated yet (all analysis fields are then `null`) AI Readiness Score (0-100) Number of detected issues Number of suggested optimizations URL checked for robots.txt URL checked for llms.txt URL checked for sitemap.xml `present`, `missing`, `error` or `invalid` Final URL after redirects Error message when status is `error` Fetched file content Per-crawler `{ status: "Allowed" | "Disallowed", reason }` map (robots.txt only) Same shape as `robotsAnalysis` (no `crawlers`) Same shape as `robotsAnalysis` (no `crawlers`) Last analysis timestamp (ISO 8601) or `null` Generated llms.txt template, or `null` if never generated ### Example Request ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands/cm1234567890abcdef/technical-analysis" \ -H "Authorization: Bearer qw-api-your-token-here" ``` ### Example Response ```json theme={null} { "success": true, "brand": { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com" }, "analyzed": true, "aiReadiness": { "score": 80, "issuesCount": 0, "optimizationsCount": 1 }, "robotsUrl": "https://mybrand.com/robots.txt", "llmsUrl": "https://mybrand.com/llms.txt", "sitemapUrl": "https://mybrand.com/sitemap.xml", "robotsAnalysis": { "status": "present", "finalUrl": "https://mybrand.com/robots.txt", "crawlers": { "GPTBot": { "status": "Allowed", "reason": "No matching disallow rule" }, "ClaudeBot": { "status": "Allowed", "reason": "No matching disallow rule" } } }, "llmsAnalysis": { "status": "missing" }, "sitemapAnalysis": { "status": "present", "finalUrl": "https://mybrand.com/sitemap.xml" }, "lastAnalyzedAt": "2026-05-12T08:30:00.000Z", "generatedLlmsTxt": null } ``` # Entities Source: https://docs.qwairy.co/developers/entities Data models and object structures returned by the API This page documents all the data entities returned by the Qwairy API. Understanding these structures will help you parse and use the data effectively. ## Pagination All list endpoints return a consistent pagination structure: ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 10, "limit": 10, "offset": 0 }, "data": [ ... ] } ``` Total number of items matching the query (across all pages) Number of items returned in this response Maximum items per page (as requested or default) Number of items skipped (for pagination) ### Pagination Example To fetch page 2 with 20 items per page: ```bash theme={null} GET /api/v1/brands/{brandId}/prompts?limit=20&offset=20 ``` Response: ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 20, "limit": 20, "offset": 20 }, "prompts": [ ... ] } ``` *** ## Brand A brand represents a company or product you're monitoring across AI platforms. List Brands, Get Brand Details ```json theme={null} { "id": "cm1234567890abcdef", "name": "My Brand", "domain": "mybrand.com", "description": "Leading provider of innovative solutions", "createdAt": "2024-01-15T10:30:00.000Z", "stats": { "promptsCount": 156, "answersCount": 312, "competitorsCount": 8 } } ``` Unique brand identifier (UUID format) Brand display name Primary domain associated with the brand Optional brand description (can be null) ISO 8601 timestamp of brand creation Total prompts tracked for this brand Total AI responses analyzed Number of competitors (SELF + DIRECT) *** ## Competitor A competitor is a brand or domain that appears in AI-generated responses. Competitors are categorized by their relationship to your brand. List Competitors, Get Competitor Details, Competitor Evolution ```json theme={null} { "id": "cmp_abc123", "name": "Competitor Name", "domain": "competitor.com", "relationship": "DIRECT", "totalMentions": 111, "shareOfVoice": 8.73, "avgPosition": 2.3, "avgSentiment": 75.2 } ``` Unique competitor identifier Competitor display name Competitor's primary domain Relationship type: * `SELF`: Your own brand * `DIRECT`: Direct competitor (used in Share of Voice calculation) * `INDIRECT`: Indirect competitor (mentioned by AI but not a direct rival) **Note:** The list and detail endpoints only return `SELF` and `DIRECT` competitors by default. However, answer details may include mentions with all three relationship types. Only `SELF` and `DIRECT` are used for Share of Voice calculations. Total number of times mentioned in AI responses Share of Voice (0-100). This entity's share of all brand mentions in AI responses: its mentions divided by the total of all SELF and DIRECT mentions. INDIRECT mentions are excluded. Average position when mentioned in lists (1 = first, lower is better) Average sentiment score (0-100, higher is more positive) *** ## Source A source is a domain cited by AI platforms when generating responses. Track which websites influence AI answers in your industry. Source Domains, Source URLs, Source Evolution ```json theme={null} { "id": "src_xyz789", "domain": "industry-news.com", "type": "MEDIA", "isSelf": false, "totalMentions": 102, "rate": 5.10, "avgPosition": 3.2 } ``` Unique source identifier Source domain name Source category: * `INSTITUTIONAL`: Official/government sites * `COMMERCIAL`: E-commerce, business sites * `MEDIA`: News, magazines, publications * `BLOG`: Personal or company blogs * `SOCIAL`: Social media platforms * `FORUM`: Discussion forums, Q\&A sites * `EDUCATIONAL`: Universities, courses * `OTHER`: Uncategorized Whether this source belongs to your brand Total citations in AI responses **Share of Citations** - percentage of all AI citations from this source (0-100). Example: `rate: 5.10` means 5.10% of all citations reference this domain. Average position in source lists (1 = first) *** ## Prompt A prompt is a question or query monitored across AI platforms. Each prompt is categorized by funnel stage and can have tags for organization. List Prompts, Get Prompt Details, Prompt Answers ```json theme={null} { "id": "q_abc123", "text": "What are the best products in this category?", "topic": "Product Reviews", "type": "TOFU", "tags": ["reviews", "comparison"], "answersCount": 2, "mentionRate": 50.0, "sourceRate": 25.0, "lastGeneratedAt": "2024-12-19T10:00:00.000Z" } ``` Unique prompt identifier The prompt/question text Associated topic/keyword name (can be null) Marketing funnel stage: * `TOFU`: Top of Funnel (awareness) * `MOFU`: Middle of Funnel (consideration) * `BOFU`: Bottom of Funnel (decision) List of tag names for organization Number of AI responses generated for this prompt Percentage of answers mentioning your brand (0-100) Percentage of answers citing your domain (0-100) ISO 8601 timestamp of last answer generation *** ## Answer An answer is an AI-generated response to a prompt. Contains the full text, detected brand mentions, and cited sources. List Answers, Get Answer Details ```json theme={null} { "id": "ans_123abc", "promptId": "q_abc123", "promptText": "What are the best products?", "provider": "ChatGPT", "model": "GPT-4o", "text": "Based on recent reviews...", "hasSelfMention": true, "selfMentionPosition": 3, "hasSelfSource": false, "competitorsCount": 5, "sourcesCount": 3, "sentiment": 82, "createdAt": "2024-12-19T10:00:00.000Z" } ``` Unique answer identifier ID of the associated prompt Text of the associated prompt AI provider name (ChatGPT, Perplexity, Claude, etc.) Specific AI model used (GPT-4o, Sonar Large, etc.) Full response text (or preview in list endpoints) Whether your brand is mentioned Position in competitor list when mentioned (null if not mentioned) Whether your domain is cited as a source Number of competitors mentioned in this answer Number of sources cited in this answer Sentiment score for your brand mention (0-100, null if not mentioned) ISO 8601 timestamp of answer generation *** ## CompetitorMention Detailed information about a competitor mention within an answer (returned in answer details). ```json theme={null} { "name": "My Brand", "position": 3, "relationship": "SELF", "sentiment": 85 } ``` Competitor name as mentioned Position in the response (1 = mentioned first) `SELF`, `DIRECT`, or `INDIRECT`. Answer details return mentions for **all** relationship types. If you're computing Share of Voice, filter to only `SELF` and `DIRECT` mentions. Sentiment score for this mention (0-100) *** ## SourceCitation Detailed information about a source citation within an answer (returned in answer details). ```json theme={null} { "url": "https://industry-news.com/article", "domain": "industry-news.com", "position": 1, "isSelf": false } ``` Full URL cited (can be null) Domain name of the source Position in source list (1 = cited first) Whether this is your own domain *** ## Topic A topic (also called keyword) groups related prompts together. ```json theme={null} { "id": "topic_abc123", "name": "Product Reviews" } ``` Unique topic identifier Topic display name *** ## Tag Tags allow custom categorization of prompts. ```json theme={null} { "id": "tag_xyz789", "name": "comparison" } ``` Unique tag identifier Tag display name *** ## Evolution Data Point Used in evolution endpoints to track metrics over time. ```json theme={null} { "date": "2024-12-01", "mentions": 5, "shareOfVoice": 7.2, "avgPosition": 2.3, "avgSentiment": 76.5 } ``` Date in YYYY-MM-DD format Number of mentions on this date Share of voice percentage on this date Average position on this date Average sentiment on this date (competitors only) # Error Codes Source: https://docs.qwairy.co/developers/errors Understanding API error responses and how to handle them The Qwairy API returns two error shapes depending on where the failure occurs: * **Resource errors** (validation and not-found errors raised inside an endpoint) return a **nested** object: `{ "error": { "code", "message" } }`. Use the `code` for programmatic handling. * **Authentication and rate-limit errors** (raised by the API gateway before the endpoint runs — 401 and 429) return a **flat** object: `{ "error", "message" }`. Here `error` is a short status label, not a machine-readable code. Always branch on the HTTP status first, then read the body in the shape matching that status. ## HTTP Status Codes | Code | Status | Description | | ---- | --------------------- | --------------------------------------- | | 200 | OK | Request succeeded | | 400 | Bad Request | Invalid request parameters | | 401 | Unauthorized | Missing or invalid authentication | | 403 | Forbidden | Valid auth but insufficient permissions | | 404 | Not Found | Resource doesn't exist | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Server-side error | ## Error Response Format ### Resource errors (nested) Validation and not-found errors raised inside an endpoint return a structured `error` object: ```json theme={null} { "error": { "code": "BRAND_NOT_FOUND", "message": "Brand not found or not accessible" } } ``` Machine-readable error code for programmatic handling Human-readable error description ### Authentication & rate-limit errors (flat) `401` (authentication) and `429` (rate limit) responses are produced by the API gateway and use a flat shape — `error` is a short status label, not a code: ```json theme={null} { "error": "Unauthorized", "message": "Missing or invalid Authorization header. Use: Authorization: Bearer qw-api-xxx" } ``` Short status label (e.g. `Unauthorized`, `Too Many Requests`). Human-readable description of the failure. 429 responses additionally include `limit` (`"burst"` or `"daily"`) and `retryAfter` (seconds). ## Error Codes Reference ### Authentication & Authorization | Code | HTTP Status | Description | | ------------------- | ----------- | ------------------------------ | | `UNAUTHORIZED` | 401 | Authentication required | | `INVALID_TOKEN` | 401 | Invalid or expired API token | | `INSUFFICIENT_PLAN` | 403 | Growth plan or higher required | | `FORBIDDEN` | 403 | Access denied | ### Not Found | Code | HTTP Status | Description | | ---------------------- | ----------- | --------------------------------- | | `BRAND_NOT_FOUND` | 404 | Brand not found or not accessible | | `COMPETITOR_NOT_FOUND` | 404 | Competitor not found | | `SOURCE_NOT_FOUND` | 404 | Source not found | | `PROMPT_NOT_FOUND` | 404 | Prompt not found | | `ANSWER_NOT_FOUND` | 404 | Answer not found | | `RESOURCE_NOT_FOUND` | 404 | Generic resource not found | | `ENDPOINT_NOT_FOUND` | 404 | API endpoint does not exist | ### Bad Request | Code | HTTP Status | Description | | -------------------- | ----------- | -------------------------- | | `INVALID_REQUEST` | 400 | Invalid request format | | `INVALID_PARAMETER` | 400 | Invalid parameter value | | `MISSING_PARAMETER` | 400 | Missing required parameter | | `INVALID_DATE_RANGE` | 400 | Invalid date range | | `INVALID_SORT_FIELD` | 400 | Invalid sort field | ### Rate Limiting | Code | HTTP Status | Description | | --------------------- | ----------- | ------------------- | | `RATE_LIMIT_EXCEEDED` | 429 | Rate limit exceeded | ### Server Errors | Code | HTTP Status | Description | | ---------------- | ----------- | --------------------- | | `INTERNAL_ERROR` | 500 | Internal server error | ## Common Errors ### Authentication Errors (401) Authentication failures use the **flat** shape: ```json theme={null} // Missing or malformed Authorization header { "error": "Unauthorized", "message": "Missing or invalid Authorization header. Use: Authorization: Bearer qw-api-xxx" } // Invalid token, revoked token, or plan below Growth { "error": "Unauthorized", "message": "Invalid API token or insufficient subscription plan (Growth+ required)" } ``` **Solution**: Ensure you're using a valid token with the correct `Bearer` prefix, on a Growth plan or higher. ### Permission Errors (403) ```json theme={null} // Team subscription doesn't include API access { "error": { "code": "INSUFFICIENT_PLAN", "message": "Growth plan or higher required" } } ``` **Solution**: Verify you have the correct permissions and plan level. ### Not Found Errors (404) ```json theme={null} // Brand doesn't exist { "error": { "code": "BRAND_NOT_FOUND", "message": "Brand not found or not accessible" } } // Competitor doesn't exist { "error": { "code": "COMPETITOR_NOT_FOUND", "message": "Competitor not found" } } ``` **Solution**: Check that the resource ID is correct and belongs to your team. ### Rate Limit Errors (429) Rate-limit responses use the **flat** shape and add `limit` and `retryAfter`: ```json theme={null} { "error": "Too Many Requests", "limit": "burst", "message": "Per-minute rate limit exceeded. Please try again in 45 seconds.", "retryAfter": 45 } ``` `limit` is `"burst"` (per-minute) or `"daily"`. **Solution**: Wait `retryAfter` seconds (or until the `X-RateLimit-Reset` / `X-RateLimit-Daily-Reset` timestamp), then retry. See [Rate Limits](/developers/rate-limits). ### Server Errors (500) ```json theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` **Solution**: Retry after a few seconds. If the error persists, contact support. ## Handling Errors ### JavaScript/TypeScript Example ```typescript theme={null} async function callQwairyAPI(endpoint: string) { const response = await fetch(`https://www.qwairy.co/api/v1${endpoint}`, { headers: { 'Authorization': `Bearer ${process.env.QWAIRY_API_TOKEN}`, }, }); if (!response.ok) { const data = await response.json().catch(() => ({})); // Auth and rate-limit errors use the flat shape: { error, message } if (response.status === 401) { throw new Error(`Authentication failed: ${data.message ?? data.error}`); } if (response.status === 429) { const retryAfter = data.retryAfter ?? response.headers.get('Retry-After'); throw new Error(`Rate limited. Retry after: ${retryAfter}s`); } // Resource errors use the nested shape: { error: { code, message } } const code = data.error?.code ?? 'UNKNOWN'; const message = data.error?.message ?? data.message ?? `HTTP ${response.status}`; switch (code) { case 'INSUFFICIENT_PLAN': case 'FORBIDDEN': throw new Error(`Permission denied: ${message}`); case 'BRAND_NOT_FOUND': case 'COMPETITOR_NOT_FOUND': case 'SOURCE_NOT_FOUND': case 'PROMPT_NOT_FOUND': case 'ANSWER_NOT_FOUND': throw new Error(`Resource not found: ${message}`); default: throw new Error(`API error [${code}]: ${message}`); } } return response.json(); } ``` ### Python Example ```python theme={null} import requests import os def call_qwairy_api(endpoint): response = requests.get( f"https://www.qwairy.co/api/v1{endpoint}", headers={"Authorization": f"Bearer {os.environ['QWAIRY_API_TOKEN']}"} ) if not response.ok: body = response.json() if response.content else {} # Auth and rate-limit errors use the flat shape: { error, message } if response.status_code == 401: raise Exception(f"Authentication failed: {body.get('message', body.get('error'))}") if response.status_code == 429: retry_after = body.get('retryAfter', response.headers.get('Retry-After')) raise Exception(f"Rate limited. Retry after: {retry_after}s") # Resource errors use the nested shape: { error: { code, message } } error = body.get('error', {}) if isinstance(body.get('error'), dict) else {} code = error.get('code', 'UNKNOWN') message = error.get('message', body.get('message', 'Unknown error')) if code in ['INSUFFICIENT_PLAN', 'FORBIDDEN']: raise Exception(f"Permission denied: {message}") elif code.endswith('_NOT_FOUND'): raise Exception(f"Not found: {message}") else: raise Exception(f"API error [{code}]: {message}") return response.json() ``` ## Need Help? If you encounter persistent errors or unexpected behavior, contact us at [team@qwairy.co](mailto:team@qwairy.co) with: * The endpoint you're calling * The full error response * Your request headers (without the token) * The timestamp of the request # Competitive Analysis Source: https://docs.qwairy.co/developers/guides/competitive-analysis Track your position against competitors and identify opportunities Analyze your competitive landscape in AI search results with gap analysis and trend tracking. This guide uses the API client from the [Guides index](/developers/guides/index#api-client). Copy it to your project first. ## What You'll Build A competitive analysis showing: * **Competitor ranking**: Share of Voice comparison * **Gap analysis**: Where you're winning and losing * **Trend tracking**: Monitor competitor momentum *** ## Get Competitive Landscape ```javascript JavaScript theme={null} async function getCompetitiveLandscape(client, brandId, period = 30) { const data = await client.getCompetitors(brandId, { period, limit: 20, sort: 'shareOfVoice', order: 'desc', }); const yourBrand = data.competitors.find(c => c.relationship === 'SELF'); const competitors = data.competitors.filter(c => c.relationship !== 'SELF'); if (!yourBrand) { throw new Error('Your brand not found in competitor data'); } return { yourPosition: { name: yourBrand.name, shareOfVoice: yourBrand.shareOfVoice, mentions: yourBrand.totalMentions, avgPosition: yourBrand.avgPosition, sentiment: yourBrand.avgSentiment, rank: data.competitors.findIndex(c => c.id === yourBrand.id) + 1, }, competitors: competitors.map(c => ({ id: c.id, name: c.name, shareOfVoice: c.shareOfVoice, mentions: c.totalMentions, avgPosition: c.avgPosition, sentiment: c.avgSentiment, gap: (c.shareOfVoice - yourBrand.shareOfVoice).toFixed(2), status: c.shareOfVoice > yourBrand.shareOfVoice ? 'ahead' : 'behind', })), summary: { totalCompetitors: competitors.length, competitorsAhead: competitors.filter(c => c.shareOfVoice > yourBrand.shareOfVoice).length, competitorsBehind: competitors.filter(c => c.shareOfVoice < yourBrand.shareOfVoice).length, avgGap: (competitors.reduce((sum, c) => sum + c.shareOfVoice, 0) / competitors.length - yourBrand.shareOfVoice).toFixed(2), }, }; } ``` ```python Python theme={null} from typing import List, Optional def get_competitive_landscape(client, brand_id: str, period: int = 30) -> dict: """Analyze competitive positioning and gaps.""" data = client.get_competitors(brand_id, period=period, limit=20, sort='shareOfVoice') your_brand = next((c for c in data['competitors'] if c['relationship'] == 'SELF'), None) competitors = [c for c in data['competitors'] if c['relationship'] != 'SELF'] if not your_brand: raise ValueError('Your brand not found in competitor data') your_sov = your_brand['shareOfVoice'] return { 'your_position': { 'name': your_brand['name'], 'share_of_voice': your_sov, 'mentions': your_brand['totalMentions'], 'avg_position': your_brand['avgPosition'], 'sentiment': your_brand['avgSentiment'], 'rank': next(i for i, c in enumerate(data['competitors']) if c['id'] == your_brand['id']) + 1, }, 'competitors': [ { 'id': c['id'], 'name': c['name'], 'share_of_voice': c['shareOfVoice'], 'mentions': c['totalMentions'], 'avg_position': c['avgPosition'], 'sentiment': c['avgSentiment'], 'gap': round(c['shareOfVoice'] - your_sov, 2), 'status': 'ahead' if c['shareOfVoice'] > your_sov else 'behind', } for c in competitors ], 'summary': { 'total_competitors': len(competitors), 'competitors_ahead': sum(1 for c in competitors if c['shareOfVoice'] > your_sov), 'competitors_behind': sum(1 for c in competitors if c['shareOfVoice'] < your_sov), 'avg_gap': round(sum(c['shareOfVoice'] for c in competitors) / len(competitors) - your_sov, 2) if competitors else 0, }, } ``` *** ## Track Competitor Evolution Monitor how a specific competitor's metrics change over time. ```javascript JavaScript theme={null} async function trackCompetitorEvolution(client, brandId, competitorId, period = 30) { const evolution = await client.getCompetitorEvolution(brandId, competitorId, { period }); const dataPoints = evolution.evolution; if (dataPoints.length < 2) { return { trend: 'insufficient_data', dataPoints }; } const first = dataPoints[0]; const last = dataPoints[dataPoints.length - 1]; const sovChange = last.shareOfVoice - first.shareOfVoice; const mentionChange = last.mentions - first.mentions; return { competitor: evolution.competitor, period: { start: first.date, end: last.date }, metrics: { sovStart: first.shareOfVoice, sovEnd: last.shareOfVoice, sovChange: sovChange.toFixed(2), sovTrend: sovChange > 0.5 ? 'growing' : sovChange < -0.5 ? 'declining' : 'stable', mentionStart: first.mentions, mentionEnd: last.mentions, mentionChange, }, dataPoints, }; } ``` ```python Python theme={null} def track_competitor_evolution(client, brand_id: str, competitor_id: str, period: int = 30) -> dict: """Track competitor metrics over time.""" evolution = client.get_competitor_evolution(brand_id, competitor_id, period=period) data_points = evolution['evolution'] if len(data_points) < 2: return {'trend': 'insufficient_data', 'data_points': data_points} first = data_points[0] last = data_points[-1] sov_change = last['shareOfVoice'] - first['shareOfVoice'] mention_change = last['mentions'] - first['mentions'] def get_trend(change: float) -> str: if change > 0.5: return 'growing' if change < -0.5: return 'declining' return 'stable' return { 'competitor': evolution['competitor'], 'period': {'start': first['date'], 'end': last['date']}, 'metrics': { 'sov_start': first['shareOfVoice'], 'sov_end': last['shareOfVoice'], 'sov_change': round(sov_change, 2), 'sov_trend': get_trend(sov_change), 'mention_start': first['mentions'], 'mention_end': last['mentions'], 'mention_change': mention_change, }, 'data_points': data_points, } ``` *** ## Usage ```javascript JavaScript theme={null} const client = new QwairyClient(process.env.QWAIRY_API_TOKEN); // Get landscape const landscape = await getCompetitiveLandscape(client, 'your-brand-id', 30); console.log(`\nYour Position: #${landscape.yourPosition.rank}`); console.log(`Share of Voice: ${landscape.yourPosition.shareOfVoice}%`); console.log(`\nCompetitors ahead: ${landscape.summary.competitorsAhead}`); console.log(`Competitors behind: ${landscape.summary.competitorsBehind}`); console.log('\nCompetitor Ranking:'); console.log('─'.repeat(60)); for (const c of landscape.competitors.slice(0, 5)) { const indicator = c.status === 'ahead' ? '↑' : '↓'; console.log(`${c.name.padEnd(25)} ${c.shareOfVoice.toFixed(1)}% SOV ${indicator} ${Math.abs(c.gap)}% gap`); } // Track top competitor const topCompetitor = landscape.competitors[0]; if (topCompetitor) { const evolution = await trackCompetitorEvolution(client, 'your-brand-id', topCompetitor.id, 30); console.log(`\n${topCompetitor.name} trend: ${evolution.metrics.sovTrend} (${evolution.metrics.sovChange}%)`); } ``` ```python Python theme={null} client = QwairyClient() # Get landscape landscape = get_competitive_landscape(client, 'your-brand-id', period=30) print(f"\nYour Position: #{landscape['your_position']['rank']}") print(f"Share of Voice: {landscape['your_position']['share_of_voice']}%") print(f"\nCompetitors ahead: {landscape['summary']['competitors_ahead']}") print(f"Competitors behind: {landscape['summary']['competitors_behind']}") print('\nCompetitor Ranking:') print('─' * 60) for c in landscape['competitors'][:5]: indicator = '↑' if c['status'] == 'ahead' else '↓' print(f"{c['name']:<25} {c['share_of_voice']:.1f}% SOV {indicator} {abs(c['gap'])}% gap") # Track top competitor if landscape['competitors']: top = landscape['competitors'][0] evolution = track_competitor_evolution(client, 'your-brand-id', top['id'], period=30) print(f"\n{top['name']} trend: {evolution['metrics']['sov_trend']} ({evolution['metrics']['sov_change']}%)") ``` *** ## Example Output **Console:** ``` Your Position: #3 Share of Voice: 8.13% Competitors ahead: 2 Competitors behind: 7 Competitor Ranking: ──────────────────────────────────────────────────────────── Competitor A 12.5% SOV ↑ 4.37% gap Competitor B 9.8% SOV ↑ 1.67% gap Competitor C 6.2% SOV ↓ 1.93% gap Competitor D 5.1% SOV ↓ 3.03% gap Competitor E 4.8% SOV ↓ 3.33% gap Competitor A trend: stable (+0.3%) ``` **JSON:** ```json theme={null} { "yourPosition": { "name": "My Brand", "shareOfVoice": 8.13, "mentions": 104, "avgPosition": 2.1, "sentiment": 78.1, "rank": 3 }, "competitors": [ { "id": "comp1", "name": "Competitor A", "shareOfVoice": 12.5, "mentions": 156, "avgPosition": 1.8, "sentiment": 72.3, "gap": "4.37", "status": "ahead" } ], "summary": { "totalCompetitors": 9, "competitorsAhead": 2, "competitorsBehind": 7, "avgGap": "-1.23" } } ``` *** ## Key Metrics | Metric | Description | Action | | ------------- | ---------------------------- | -------------------------------- | | `gap` | Difference in Share of Voice | Positive = competitor ahead | | `avgPosition` | Average rank in AI responses | Lower = better (1 is first) | | `status` | Relative position | `ahead` or `behind` your brand | | `sovTrend` | Direction over time | `growing`, `declining`, `stable` | *** ## Next Steps * Build a [custom dashboard](/developers/guides/custom-dashboard) to visualize these insights * Set up [weekly reports](/developers/guides/weekly-reports) to track changes * [Export data](/developers/guides/data-export) for deeper analysis in BI tools # Custom Dashboard Source: https://docs.qwairy.co/developers/guides/custom-dashboard Build a branded GEO dashboard with KPIs, competitor rankings, and source analysis Build a custom dashboard displaying your brand's GEO performance metrics, competitor rankings, and top sources. This guide uses the API client from the [Guides index](/developers/guides/index#api-client). Copy it to your project first. ## What You'll Build A dashboard with: * **KPI cards**: Mention Rate, Source Rate, Coverage, Share of Voice, Sentiment * **Competitor ranking**: Share of Voice comparison with gap analysis * **Source ranking**: Share of Citations by domain *** ## Fetch Dashboard Data Fetch all data in parallel for optimal performance. ```javascript JavaScript theme={null} async function fetchDashboardData(client, brandId, period = 30) { // Parallel API calls const [performance, competitors, sources] = await Promise.all([ client.getPerformance(brandId, { period }), client.getCompetitors(brandId, { period, limit: 10, sort: 'shareOfVoice', order: 'desc' }), client.getSourceDomains(brandId, { period, limit: 10, sort: 'mentions', order: 'desc' }), ]); // Find your brand in competitors list const yourBrand = competitors.competitors.find(c => c.relationship === 'SELF'); const directCompetitors = competitors.competitors.filter(c => c.relationship === 'DIRECT'); return { kpis: { mentionRate: performance.scores.mentionRate, sourceRate: performance.scores.sourceRate, coverage: performance.scores.coverage, shareOfVoice: performance.scores.shareOfVoice, sentiment: performance.scores.sentiment, }, yourBrand: yourBrand ? { name: yourBrand.name, mentions: yourBrand.totalMentions, shareOfVoice: yourBrand.shareOfVoice, avgPosition: yourBrand.avgPosition, sentiment: yourBrand.avgSentiment, } : null, competitorRanking: directCompetitors.map(c => ({ name: c.name, shareOfVoice: c.shareOfVoice, mentions: c.totalMentions, gap: yourBrand ? (c.shareOfVoice - yourBrand.shareOfVoice).toFixed(2) : null, })), sourceRanking: sources.sources.map(s => ({ domain: s.domain, shareOfCitations: s.rate, mentions: s.totalMentions, isSelf: s.isSelf, })), methodology: { period: `${period} days`, promptsAnalyzed: performance.methodology.promptsCount, responsesGenerated: performance.methodology.responsesTotal, providers: performance.methodology.providers, }, }; } ``` ```python Python theme={null} from dataclasses import dataclass from typing import List, Optional from concurrent.futures import ThreadPoolExecutor @dataclass class DashboardData: kpis: dict your_brand: Optional[dict] competitor_ranking: List[dict] source_ranking: List[dict] methodology: dict def fetch_dashboard_data(client, brand_id: str, period: int = 30) -> DashboardData: """Fetch all dashboard data in parallel.""" with ThreadPoolExecutor(max_workers=3) as executor: perf_future = executor.submit(client.get_performance, brand_id, period=period) comp_future = executor.submit(client.get_competitors, brand_id, period=period, limit=10, sort='shareOfVoice') src_future = executor.submit(client.get_source_domains, brand_id, period=period, limit=10, sort='mentions') performance = perf_future.result() competitors = comp_future.result() sources = src_future.result() your_brand = next((c for c in competitors['competitors'] if c['relationship'] == 'SELF'), None) direct_competitors = [c for c in competitors['competitors'] if c['relationship'] == 'DIRECT'] return DashboardData( kpis={ 'mention_rate': performance['scores']['mentionRate'], 'source_rate': performance['scores']['sourceRate'], 'coverage': performance['scores']['coverage'], 'share_of_voice': performance['scores']['shareOfVoice'], 'sentiment': performance['scores']['sentiment'], }, your_brand={ 'name': your_brand['name'], 'mentions': your_brand['totalMentions'], 'share_of_voice': your_brand['shareOfVoice'], 'avg_position': your_brand['avgPosition'], 'sentiment': your_brand['avgSentiment'], } if your_brand else None, competitor_ranking=[ { 'name': c['name'], 'share_of_voice': c['shareOfVoice'], 'mentions': c['totalMentions'], 'gap': round(c['shareOfVoice'] - your_brand['shareOfVoice'], 2) if your_brand else None, } for c in direct_competitors ], source_ranking=[ { 'domain': s['domain'], 'share_of_citations': s['rate'], 'mentions': s['totalMentions'], 'is_self': s['isSelf'], } for s in sources['sources'] ], methodology={ 'period': f"{period} days", 'prompts_analyzed': performance['methodology']['promptsCount'], 'responses_generated': performance['methodology']['responsesTotal'], 'providers': performance['methodology']['providers'], }, ) ``` *** ## Usage ```javascript JavaScript theme={null} const client = new QwairyClient(process.env.QWAIRY_API_TOKEN); const dashboard = await fetchDashboardData(client, 'your-brand-id', 30); console.log('KPIs:', dashboard.kpis); console.log('Your position:', dashboard.yourBrand); console.log('Top competitors:', dashboard.competitorRanking); console.log('Top sources:', dashboard.sourceRanking); ``` ```python Python theme={null} client = QwairyClient() dashboard = fetch_dashboard_data(client, 'your-brand-id', period=30) print(f"Mention Rate: {dashboard.kpis['mention_rate']}%") print(f"Your Share of Voice: {dashboard.your_brand['share_of_voice']}%") print(f"Top competitor gap: {dashboard.competitor_ranking[0]['gap']}%") ``` *** ## Example Output ```json theme={null} { "kpis": { "mentionRate": 45.2, "sourceRate": 23.9, "coverage": 33.33, "shareOfVoice": 8.13, "sentiment": 78.1 }, "yourBrand": { "name": "My Brand", "mentions": 104, "shareOfVoice": 8.13, "avgPosition": 2.1, "sentiment": 78.1 }, "competitorRanking": [ { "name": "Competitor A", "shareOfVoice": 12.5, "mentions": 156, "gap": "4.37" }, { "name": "Competitor B", "shareOfVoice": 9.8, "mentions": 122, "gap": "1.67" } ], "sourceRanking": [ { "domain": "industry-news.com", "shareOfCitations": 5.10, "mentions": 102, "isSelf": false }, { "domain": "mybrand.com", "shareOfCitations": 2.25, "mentions": 45, "isSelf": true } ], "methodology": { "period": "30 days", "promptsAnalyzed": 156, "responsesGenerated": 312, "providers": ["chatgpt", "perplexity"] } } ``` *** ## Data Structure Reference | Field | Type | Description | | ---------------------------------- | ------- | ----------------------------------------------------- | | `kpis.mentionRate` | number | % of brand-mentioning AI responses that mention yours | | `kpis.sourceRate` | number | % of source-citing AI responses that cite your domain | | `kpis.coverage` | number | % of all monitored AI responses mentioning your brand | | `kpis.shareOfVoice` | number | Your % of all brand mentions (SELF + DIRECT) | | `kpis.sentiment` | number | Average sentiment score (0-100) | | `competitorRanking[].gap` | string | Difference in Share of Voice vs your brand | | `sourceRanking[].shareOfCitations` | number | Source's % of all citations (`rate` field) | | `sourceRanking[].isSelf` | boolean | Whether this is your own domain | *** ## LLM Diagnostics (Optional) Go beyond aggregate scores: identify responses where your brand is mentioned but not cited (missed link opportunities), and discover what web queries AI models trigger before answering. ```javascript JavaScript theme={null} async function fetchDiagnostics(client, brandId, period = 30) { const [mentionedNotCited, searchQueries] = await Promise.all([ client.getAnswers(brandId, { period, hasSelfMention: true, hasSelfSource: false, limit: 5, sort: 'createdAt', order: 'desc', }), client.getSearch(brandId, { period, limit: 5, sort: 'createdAt', order: 'desc' }), ]); return { // Responses where AI mentions you but doesn't link to your content missedCitations: mentionedNotCited.answers.map(a => ({ prompt: a.promptText, provider: a.provider, position: a.selfMentionPosition, competitors: a.competitorsCount, })), // Web queries AI models run before generating a response aiSearchQueries: searchQueries.searches.map(s => ({ query: s.query, prompt: s.prompt, provider: s.provider, })), }; } ``` ```python Python theme={null} def fetch_diagnostics(client, brand_id: str, period: int = 30) -> dict: """Fetch LLM diagnostic data.""" from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=2) as executor: mention_future = executor.submit( client.get_answers, brand_id, period=period, has_self_mention=True, has_self_source=False, limit=5, ) search_future = executor.submit( client.get_search, brand_id, period=period, limit=5, ) mentioned_not_cited = mention_future.result() search_queries = search_future.result() return { 'missed_citations': [ { 'prompt': a['promptText'], 'provider': a['provider'], 'position': a.get('selfMentionPosition'), 'competitors': a['competitorsCount'], } for a in mentioned_not_cited['answers'] ], 'ai_search_queries': [ { 'query': s['query'], 'prompt': s['prompt'], 'provider': s.get('provider'), } for s in search_queries['searches'] ], } ``` **Missed citations** reveal where AI talks about you but doesn't link to your content — your highest-ROI content opportunities. **AI search queries** show what AI models actually search for before responding, giving you direct content targeting signals. *** ## Next Steps * Add [weekly reports](/developers/guides/weekly-reports) to track changes over time * Use [competitive analysis](/developers/guides/competitive-analysis) for deeper insights * [Export data](/developers/guides/data-export) to your BI tools # Data Export Source: https://docs.qwairy.co/developers/guides/data-export Export GEO data to CSV, JSON, and BI tools like BigQuery or Snowflake Export your Qwairy data for analysis in spreadsheets, databases, or BI platforms. This guide uses the API client from the [Guides index](/developers/guides/index#api-client). Copy it to your project first. ## What You'll Build Export pipelines for: * **CSV files**: For Excel, Google Sheets * **JSON files**: For data pipelines * **BigQuery**: For Google Cloud analytics * **Snowflake**: For enterprise data warehousing *** ## Fetch All Data First, gather data from all endpoints. ```javascript JavaScript theme={null} async function fetchAllData(client, brandId, period = 30) { const [performance, competitors, sources] = await Promise.all([ client.getPerformance(brandId, { period }), client.getCompetitors(brandId, { period, limit: 100 }), client.getSourceDomains(brandId, { period, limit: 100 }), ]); return { exportedAt: new Date().toISOString(), brandId, period, performance: { scores: performance.scores, methodology: performance.methodology, }, competitors: competitors.competitors.map(c => ({ id: c.id, name: c.name, relationship: c.relationship, shareOfVoice: c.shareOfVoice, totalMentions: c.totalMentions, avgPosition: c.avgPosition, avgSentiment: c.avgSentiment, })), sources: sources.sources.map(s => ({ id: s.id, domain: s.domain, type: s.type, isSelf: s.isSelf, totalMentions: s.totalMentions, rate: s.rate, avgPosition: s.avgPosition, })), }; } ``` ```python Python theme={null} def fetch_all_data(client, brand_id: str, period: int = 30) -> dict: """Fetch all data for export.""" from concurrent.futures import ThreadPoolExecutor from datetime import datetime with ThreadPoolExecutor(max_workers=3) as executor: perf_future = executor.submit(client.get_performance, brand_id, period=period) comp_future = executor.submit(client.get_competitors, brand_id, period=period, limit=100) src_future = executor.submit(client.get_source_domains, brand_id, period=period, limit=100) performance = perf_future.result() competitors = comp_future.result() sources = src_future.result() return { 'exported_at': datetime.now().isoformat(), 'brand_id': brand_id, 'period': period, 'performance': { 'scores': performance['scores'], 'methodology': performance['methodology'], }, 'competitors': [ { 'id': c['id'], 'name': c['name'], 'relationship': c['relationship'], 'share_of_voice': c['shareOfVoice'], 'total_mentions': c['totalMentions'], 'avg_position': c['avgPosition'], 'avg_sentiment': c['avgSentiment'], } for c in competitors['competitors'] ], 'sources': [ { 'id': s['id'], 'domain': s['domain'], 'type': s['type'], 'is_self': s['isSelf'], 'total_mentions': s['totalMentions'], 'rate': s['rate'], 'avg_position': s['avgPosition'], } for s in sources['sources'] ], } ``` *** ## Export to CSV ```javascript JavaScript theme={null} const fs = require('fs'); function exportToCSV(data, outputDir = './exports') { if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const timestamp = data.exportedAt.split('T')[0]; // Competitors CSV const competitorHeaders = ['id', 'name', 'relationship', 'shareOfVoice', 'totalMentions', 'avgPosition', 'avgSentiment']; const competitorRows = data.competitors.map(c => competitorHeaders.map(h => c[h]).join(',')); fs.writeFileSync( `${outputDir}/competitors_${timestamp}.csv`, [competitorHeaders.join(','), ...competitorRows].join('\n') ); // Sources CSV const sourceHeaders = ['id', 'domain', 'type', 'isSelf', 'totalMentions', 'rate', 'avgPosition']; const sourceRows = data.sources.map(s => sourceHeaders.map(h => s[h]).join(',')); fs.writeFileSync( `${outputDir}/sources_${timestamp}.csv`, [sourceHeaders.join(','), ...sourceRows].join('\n') ); // Performance CSV (single row) const perfHeaders = ['mentionRate', 'sourceRate', 'coverage', 'shareOfVoice', 'sentiment']; const perfValues = [ data.performance.scores.mentionRate, data.performance.scores.sourceRate, data.performance.scores.coverage, data.performance.scores.shareOfVoice, data.performance.scores.sentiment, ]; fs.writeFileSync( `${outputDir}/performance_${timestamp}.csv`, [perfHeaders.join(','), perfValues.join(',')].join('\n') ); return { files: [ `${outputDir}/competitors_${timestamp}.csv`, `${outputDir}/sources_${timestamp}.csv`, `${outputDir}/performance_${timestamp}.csv`, ], }; } ``` ```python Python theme={null} import csv import os def export_to_csv(data: dict, output_dir: str = './exports') -> dict: """Export data to CSV files.""" os.makedirs(output_dir, exist_ok=True) timestamp = data['exported_at'].split('T')[0] files = [] # Competitors CSV comp_file = f'{output_dir}/competitors_{timestamp}.csv' with open(comp_file, 'w', newline='') as f: if data['competitors']: writer = csv.DictWriter(f, fieldnames=data['competitors'][0].keys()) writer.writeheader() writer.writerows(data['competitors']) files.append(comp_file) # Sources CSV src_file = f'{output_dir}/sources_{timestamp}.csv' with open(src_file, 'w', newline='') as f: if data['sources']: writer = csv.DictWriter(f, fieldnames=data['sources'][0].keys()) writer.writeheader() writer.writerows(data['sources']) files.append(src_file) # Performance CSV perf_file = f'{output_dir}/performance_{timestamp}.csv' scores = data['performance']['scores'] with open(perf_file, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['mention_rate', 'source_rate', 'coverage', 'share_of_voice', 'sentiment']) writer.writeheader() writer.writerow({ 'mention_rate': scores['mentionRate'], 'source_rate': scores['sourceRate'], 'coverage': scores['coverage'], 'share_of_voice': scores['shareOfVoice'], 'sentiment': scores['sentiment'], }) files.append(perf_file) return {'files': files} ``` *** ## Export to JSON ```javascript JavaScript theme={null} const fs = require('fs'); function exportToJSON(data, outputDir = './exports') { if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const timestamp = data.exportedAt.split('T')[0]; const filepath = `${outputDir}/qwairy_export_${timestamp}.json`; fs.writeFileSync(filepath, JSON.stringify(data, null, 2)); return { file: filepath, size: fs.statSync(filepath).size }; } ``` ```python Python theme={null} import json import os def export_to_json(data: dict, output_dir: str = './exports') -> dict: """Export data to JSON file.""" os.makedirs(output_dir, exist_ok=True) timestamp = data['exported_at'].split('T')[0] filepath = f'{output_dir}/qwairy_export_{timestamp}.json' with open(filepath, 'w') as f: json.dump(data, f, indent=2) return {'file': filepath, 'size': os.path.getsize(filepath)} ``` *** ## Database Schemas Create these tables before exporting to BigQuery or Snowflake. ```sql BigQuery theme={null} -- Create dataset CREATE SCHEMA IF NOT EXISTS qwairy; -- Competitors table CREATE TABLE IF NOT EXISTS qwairy.competitors ( id STRING NOT NULL, name STRING NOT NULL, relationship STRING, share_of_voice FLOAT64, total_mentions INT64, avg_position FLOAT64, avg_sentiment FLOAT64, exported_at TIMESTAMP NOT NULL, brand_id STRING NOT NULL ); -- Sources table CREATE TABLE IF NOT EXISTS qwairy.sources ( id STRING NOT NULL, domain STRING NOT NULL, type STRING, is_self BOOL, total_mentions INT64, rate FLOAT64, avg_position FLOAT64, exported_at TIMESTAMP NOT NULL, brand_id STRING NOT NULL ); -- Performance table CREATE TABLE IF NOT EXISTS qwairy.performance ( mentionRate FLOAT64, sourceRate FLOAT64, coverage FLOAT64, shareOfVoice FLOAT64, sentiment FLOAT64, methodology STRING, exported_at TIMESTAMP NOT NULL, brand_id STRING NOT NULL ); ``` ```sql Snowflake theme={null} -- Create schema CREATE SCHEMA IF NOT EXISTS QWAIRY; -- Competitors table CREATE TABLE IF NOT EXISTS QWAIRY.COMPETITORS ( ID VARCHAR(50) NOT NULL, NAME VARCHAR(255) NOT NULL, RELATIONSHIP VARCHAR(50), SHARE_OF_VOICE FLOAT, TOTAL_MENTIONS INTEGER, AVG_POSITION FLOAT, AVG_SENTIMENT FLOAT, EXPORTED_AT TIMESTAMP_NTZ NOT NULL, BRAND_ID VARCHAR(50) NOT NULL ); -- Sources table CREATE TABLE IF NOT EXISTS QWAIRY.SOURCES ( ID VARCHAR(50) NOT NULL, DOMAIN VARCHAR(255) NOT NULL, TYPE VARCHAR(50), IS_SELF BOOLEAN, TOTAL_MENTIONS INTEGER, RATE FLOAT, AVG_POSITION FLOAT, EXPORTED_AT TIMESTAMP_NTZ NOT NULL, BRAND_ID VARCHAR(50) NOT NULL ); -- Performance table CREATE TABLE IF NOT EXISTS QWAIRY.PERFORMANCE ( MENTION_RATE FLOAT, SOURCE_RATE FLOAT, COVERAGE FLOAT, SHARE_OF_VOICE FLOAT, SENTIMENT FLOAT, METHODOLOGY VARCHAR(5000), EXPORTED_AT TIMESTAMP_NTZ NOT NULL, BRAND_ID VARCHAR(50) NOT NULL ); ``` *** ## Export to BigQuery ```javascript JavaScript theme={null} const { BigQuery } = require('@google-cloud/bigquery'); async function exportToBigQuery(data, datasetId = 'qwairy', projectId = process.env.GCP_PROJECT_ID) { const bigquery = new BigQuery({ projectId }); // Create dataset if not exists const [datasets] = await bigquery.getDatasets(); if (!datasets.find(d => d.id === datasetId)) { await bigquery.createDataset(datasetId); } const dataset = bigquery.dataset(datasetId); // Insert competitors await dataset.table('competitors').insert( data.competitors.map(c => ({ ...c, exportedAt: data.exportedAt, brandId: data.brandId, })) ); // Insert sources await dataset.table('sources').insert( data.sources.map(s => ({ ...s, exportedAt: data.exportedAt, brandId: data.brandId, })) ); // Insert performance await dataset.table('performance').insert([{ ...data.performance.scores, methodology: JSON.stringify(data.performance.methodology), exportedAt: data.exportedAt, brandId: data.brandId, }]); return { dataset: datasetId, tables: ['competitors', 'sources', 'performance'] }; } ``` ```python Python theme={null} from google.cloud import bigquery def export_to_bigquery(data: dict, dataset_id: str = 'qwairy', project_id: str = None) -> dict: """Export data to BigQuery.""" import os import json project_id = project_id or os.environ.get('GCP_PROJECT_ID') client = bigquery.Client(project=project_id) # Create dataset if not exists dataset_ref = client.dataset(dataset_id) try: client.get_dataset(dataset_ref) except Exception: client.create_dataset(dataset_ref) # Insert competitors competitors_table = f'{project_id}.{dataset_id}.competitors' rows = [ {**c, 'exported_at': data['exported_at'], 'brand_id': data['brand_id']} for c in data['competitors'] ] if rows: client.insert_rows_json(competitors_table, rows) # Insert sources sources_table = f'{project_id}.{dataset_id}.sources' rows = [ {**s, 'exported_at': data['exported_at'], 'brand_id': data['brand_id']} for s in data['sources'] ] if rows: client.insert_rows_json(sources_table, rows) # Insert performance perf_table = f'{project_id}.{dataset_id}.performance' perf_row = { **data['performance']['scores'], 'methodology': json.dumps(data['performance']['methodology']), 'exported_at': data['exported_at'], 'brand_id': data['brand_id'], } client.insert_rows_json(perf_table, [perf_row]) return {'dataset': dataset_id, 'tables': ['competitors', 'sources', 'performance']} ``` *** ## Export to Snowflake ```javascript JavaScript theme={null} const snowflake = require('snowflake-sdk'); async function exportToSnowflake(data, config) { const connection = snowflake.createConnection({ account: config.account, username: config.username, password: config.password, warehouse: config.warehouse, database: config.database, schema: config.schema || 'QWAIRY', }); await new Promise((resolve, reject) => { connection.connect((err) => err ? reject(err) : resolve()); }); const execute = (sql, binds = []) => new Promise((resolve, reject) => { connection.execute({ sqlText: sql, binds, complete: (err, stmt, rows) => err ? reject(err) : resolve(rows) }); }); // Insert competitors for (const c of data.competitors) { await execute( `INSERT INTO competitors (id, name, relationship, share_of_voice, total_mentions, avg_position, avg_sentiment, exported_at, brand_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [c.id, c.name, c.relationship, c.shareOfVoice, c.totalMentions, c.avgPosition, c.avgSentiment, data.exportedAt, data.brandId] ); } // Insert sources for (const s of data.sources) { await execute( `INSERT INTO sources (id, domain, type, is_self, total_mentions, rate, avg_position, exported_at, brand_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [s.id, s.domain, s.type, s.isSelf, s.totalMentions, s.rate, s.avgPosition, data.exportedAt, data.brandId] ); } connection.destroy(); return { schema: config.schema || 'QWAIRY', tables: ['competitors', 'sources'] }; } ``` ```python Python theme={null} import snowflake.connector def export_to_snowflake(data: dict, config: dict) -> dict: """Export data to Snowflake.""" conn = snowflake.connector.connect( account=config['account'], user=config['username'], password=config['password'], warehouse=config['warehouse'], database=config['database'], schema=config.get('schema', 'QWAIRY'), ) cursor = conn.cursor() # Insert competitors for c in data['competitors']: cursor.execute( '''INSERT INTO competitors (id, name, relationship, share_of_voice, total_mentions, avg_position, avg_sentiment, exported_at, brand_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)''', (c['id'], c['name'], c['relationship'], c['share_of_voice'], c['total_mentions'], c['avg_position'], c['avg_sentiment'], data['exported_at'], data['brand_id']) ) # Insert sources for s in data['sources']: cursor.execute( '''INSERT INTO sources (id, domain, type, is_self, total_mentions, rate, avg_position, exported_at, brand_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)''', (s['id'], s['domain'], s['type'], s['is_self'], s['total_mentions'], s['rate'], s['avg_position'], data['exported_at'], data['brand_id']) ) conn.commit() cursor.close() conn.close() return {'schema': config.get('schema', 'QWAIRY'), 'tables': ['competitors', 'sources']} ``` *** ## Usage ```javascript JavaScript theme={null} const client = new QwairyClient(process.env.QWAIRY_API_TOKEN); // Fetch data const data = await fetchAllData(client, 'your-brand-id', 30); // Export to files const csvResult = exportToCSV(data); console.log('CSV files:', csvResult.files); const jsonResult = exportToJSON(data); console.log('JSON file:', jsonResult.file, `(${jsonResult.size} bytes)`); // Export to BigQuery (requires @google-cloud/bigquery) // const bqResult = await exportToBigQuery(data); // console.log('BigQuery tables:', bqResult.tables); ``` ```python Python theme={null} client = QwairyClient() # Fetch data data = fetch_all_data(client, 'your-brand-id', period=30) # Export to files csv_result = export_to_csv(data) print(f"CSV files: {csv_result['files']}") json_result = export_to_json(data) print(f"JSON file: {json_result['file']} ({json_result['size']} bytes)") # Export to BigQuery (requires google-cloud-bigquery) # bq_result = export_to_bigquery(data) # print(f"BigQuery tables: {bq_result['tables']}") ``` *** ## Scheduling Exports Automate daily or weekly exports: | Platform | Configuration | | ------------------ | --------------------------------- | | **Cron** | `0 1 * * *` (daily at 1am) | | **GitHub Actions** | `schedule: cron: '0 1 * * *'` | | **AWS Lambda** | EventBridge scheduled rule | | **Google Cloud** | Cloud Scheduler + Cloud Functions | *** ## Next Steps * Create a [custom dashboard](/developers/guides/custom-dashboard) for real-time monitoring * Set up [weekly reports](/developers/guides/weekly-reports) for stakeholders * Add [competitive analysis](/developers/guides/competitive-analysis) to your exports # Quick Start Source: https://docs.qwairy.co/developers/guides/index Make your first Qwairy API call, then build on the shared client and guides Go from an API token to your brand's performance scores in one call, then reuse the shared client and the worked guides for full integrations. You need an API token (`Bearer qw-api-...`, Growth plan or above) and your brand ID. See [Authentication](/developers/authentication) to create a token and [List Brands](/developers/endpoints/brands/list) to find your brand ID. ## Quick Start Get your brand's performance scores in one command: ```bash cURL theme={null} curl -s -H "Authorization: Bearer $QWAIRY_API_TOKEN" \ "https://www.qwairy.co/api/v1/brands/YOUR_BRAND_ID/performance?period=30" \ | jq '.scores' ``` ```javascript Node.js theme={null} const response = await fetch( 'https://www.qwairy.co/api/v1/brands/YOUR_BRAND_ID/performance?period=30', { headers: { 'Authorization': `Bearer ${process.env.QWAIRY_API_TOKEN}` } } ); const { scores } = await response.json(); console.log(`Mention Rate: ${scores.mentionRate}% | Share of Voice: ${scores.shareOfVoice}%`); ``` ```python Python theme={null} import requests import os response = requests.get( 'https://www.qwairy.co/api/v1/brands/YOUR_BRAND_ID/performance?period=30', headers={'Authorization': f'Bearer {os.environ["QWAIRY_API_TOKEN"]}'} ) scores = response.json()['scores'] print(f"Mention Rate: {scores['mentionRate']}% | Share of Voice: {scores['shareOfVoice']}%") ``` **Output:** ```json theme={null} { "mentionRate": 45.2, "mentionCount": 104, "mentionTotal": 230, "coverage": 33.33, "sourceRate": 23.9, "sourceCount": 44, "sourceTotal": 184, "sourcePages": 27, "sentiment": 78.1, "shareOfVoice": 8.13 } ``` *** ## Guides Worked, copy-pasteable integrations built on the shared client below. Build a branded GEO dashboard with KPIs and competitor rankings Automate week-over-week performance reports Track your position against competitors Export data for BI tools (BigQuery, Snowflake, CSV) *** ## API Client Reusable client with error handling for all guides on this site. ```javascript JavaScript theme={null} // qwairy-client.js class QwairyClient { constructor(apiToken) { this.baseUrl = 'https://www.qwairy.co/api/v1'; this.headers = { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json', }; } async request(endpoint, params = {}) { const url = new URL(`${this.baseUrl}${endpoint}`); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { url.searchParams.append(key, value); } }); const response = await fetch(url.toString(), { headers: this.headers }); if (!response.ok) { const error = await response.json().catch(() => ({})); const code = error.error?.code || 'UNKNOWN_ERROR'; const message = error.error?.message || `HTTP ${response.status}`; if (response.status === 429) { const resetTime = response.headers.get('X-RateLimit-Reset'); throw new Error(`Rate limited. Retry after: ${resetTime}`); } throw new Error(`[${code}] ${message}`); } return response.json(); } async getPerformance(brandId, params = {}) { return this.request(`/brands/${brandId}/performance`, params); } async getCompetitors(brandId, params = {}) { return this.request(`/brands/${brandId}/competitors`, params); } async getSourceDomains(brandId, params = {}) { return this.request(`/brands/${brandId}/source-domains`, params); } async getSourceUrls(brandId, params = {}) { return this.request(`/brands/${brandId}/source-urls`, params); } async getAnswers(brandId, params = {}) { return this.request(`/brands/${brandId}/answers`, params); } async getSearch(brandId, params = {}) { return this.request(`/brands/${brandId}/search`, params); } async getPerception(brandId, params = {}) { return this.request(`/brands/${brandId}/perception`, params); } async getContent(brandId, params = {}) { return this.request(`/brands/${brandId}/content`, params); } async getTechnicalAnalysis(brandId, params = {}) { return this.request(`/brands/${brandId}/technical-analysis`, params); } async getCompetitorEvolution(brandId, competitorId, params = {}) { return this.request(`/brands/${brandId}/competitors/${competitorId}/evolution`, params); } } // Usage const client = new QwairyClient(process.env.QWAIRY_API_TOKEN); ``` ```python Python theme={null} # qwairy_client.py import requests import os from typing import Optional, Dict, Any class QwairyError(Exception): """Custom exception for Qwairy API errors.""" def __init__(self, code: str, message: str, status_code: int): self.code = code self.message = message self.status_code = status_code super().__init__(f"[{code}] {message}") class QwairyClient: def __init__(self, api_token: Optional[str] = None): self.base_url = 'https://www.qwairy.co/api/v1' self.api_token = api_token or os.environ.get('QWAIRY_API_TOKEN') if not self.api_token: raise ValueError("API token required. Set QWAIRY_API_TOKEN or pass to constructor.") self.session = requests.Session() self.session.headers.update({ 'Authorization': f'Bearer {self.api_token}', 'Content-Type': 'application/json', }) def _request(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]: url = f"{self.base_url}{endpoint}" clean_params = {k: v for k, v in (params or {}).items() if v is not None} response = self.session.get(url, params=clean_params) if not response.ok: error_data = response.json() if response.content else {} error = error_data.get('error', {}) code = error.get('code', 'UNKNOWN_ERROR') message = error.get('message', f'HTTP {response.status_code}') if response.status_code == 429: reset_time = response.headers.get('X-RateLimit-Reset', 'unknown') raise QwairyError('RATE_LIMITED', f'Retry after: {reset_time}', 429) raise QwairyError(code, message, response.status_code) return response.json() def get_performance(self, brand_id: str, period: Optional[int] = None, start_date: Optional[str] = None, end_date: Optional[str] = None) -> Dict: return self._request(f'/brands/{brand_id}/performance', { 'period': period, 'startDate': start_date, 'endDate': end_date, }) def get_competitors(self, brand_id: str, period: Optional[int] = None, limit: int = 50, offset: int = 0, sort: str = 'mentions', order: str = 'desc') -> Dict: return self._request(f'/brands/{brand_id}/competitors', { 'period': period, 'limit': limit, 'offset': offset, 'sort': sort, 'order': order, }) def get_source_domains(self, brand_id: str, period: Optional[int] = None, limit: int = 50, offset: int = 0, sort: str = 'mentions', order: str = 'desc') -> Dict: return self._request(f'/brands/{brand_id}/source-domains', { 'period': period, 'limit': limit, 'offset': offset, 'sort': sort, 'order': order, }) def get_source_urls(self, brand_id: str, period: Optional[int] = None, limit: int = 50, offset: int = 0, sort: str = 'mentions', order: str = 'desc') -> Dict: return self._request(f'/brands/{brand_id}/source-urls', { 'period': period, 'limit': limit, 'offset': offset, 'sort': sort, 'order': order, }) def get_answers(self, brand_id: str, period: Optional[int] = None, limit: int = 50, offset: int = 0, provider: Optional[str] = None, has_self_mention: Optional[bool] = None, has_self_source: Optional[bool] = None) -> Dict: return self._request(f'/brands/{brand_id}/answers', { 'period': period, 'limit': limit, 'offset': offset, 'provider': provider, 'hasSelfMention': has_self_mention, 'hasSelfSource': has_self_source, }) def get_search(self, brand_id: str, period: Optional[int] = None, limit: int = 50, offset: int = 0, provider: Optional[str] = None) -> Dict: return self._request(f'/brands/{brand_id}/search', { 'period': period, 'limit': limit, 'offset': offset, 'provider': provider, }) def get_perception(self, brand_id: str, months: Optional[int] = None) -> Dict: return self._request(f'/brands/{brand_id}/perception', { 'months': months, }) def get_content(self, brand_id: str, status: Optional[str] = None, limit: int = 50, offset: int = 0) -> Dict: return self._request(f'/brands/{brand_id}/content', { 'status': status, 'limit': limit, 'offset': offset, }) def get_technical_analysis(self, brand_id: str) -> Dict: return self._request(f'/brands/{brand_id}/technical-analysis', {}) def get_competitor_evolution(self, brand_id: str, competitor_id: str, period: Optional[int] = None) -> Dict: return self._request(f'/brands/{brand_id}/competitors/{competitor_id}/evolution', { 'period': period, }) # Usage client = QwairyClient() ``` All guides on this site use this client. Copy it to your project or adapt it to your needs. *** ## TypeScript Types Type definitions for the API client. ```typescript theme={null} // qwairy-types.ts interface QwairyScores { mentionRate: number; mentionCount: number; mentionTotal: number; coverage: number; sourceRate: number; sourceCount: number; sourceTotal: number; sourcePages: number; sentiment: number; shareOfVoice: number; } interface QwairyMethodology { promptsCount: number; providersCount: number; providers: string[]; responsesTotal: number; responsesWithMentions: number; responsesWithSources: number; } interface QwairyTopicBreakdown { id: string; topic: string; score: number; mentionRate: number; sourceRate: number; shareOfVoice: number; avgSentiment: number | null; promptsCount: number; } interface QwairyTagBreakdown { id: string; name: string; score: number; mentionRate: number; sourceRate: number; shareOfVoice: number; avgSentiment: number | null; promptsCount: number; } interface QwairyPerformance { success: boolean; brand: { id: string; name: string; domain: string | null }; period: { start: string; end: string }; scores: QwairyScores; methodology: QwairyMethodology; topCompetitors: QwairyCompetitor[]; topSources: QwairySource[]; byTopic?: QwairyTopicBreakdown[]; byTag?: QwairyTagBreakdown[]; } interface QwairyCompetitor { id: string; name: string; relationship: 'SELF' | 'DIRECT' | 'INDIRECT'; totalMentions: number; shareOfVoice: number; avgPosition: number; avgSentiment: number; } interface QwairySource { id: string; domain: string; type: 'INSTITUTIONAL' | 'COMMERCIAL' | 'MEDIA' | 'BLOG' | 'SOCIAL' | 'OTHER'; isSelf: boolean; totalMentions: number; rate: number; avgPosition: number; } interface QwairyPagination { total: number; count: number; limit: number; offset: number; } interface QwairyCompetitorsResponse { success: boolean; pagination: QwairyPagination; competitors: QwairyCompetitor[]; } interface QwairySourcesResponse { success: boolean; pagination: QwairyPagination; sources: QwairySource[]; } interface QwairyAnswer { id: string; promptId: string; promptText: string; provider: string; model: string; textPreview: string; hasSelfMention: boolean; selfMentionPosition: number | null; hasSelfSource: boolean; competitorsCount: number; sourcesCount: number; sentiment: number; relevance: number; createdAt: string; } interface QwairySearchQuery { id: string; query: string; prompt: string; provider: string; createdAt: string; } interface QwairyAnswersResponse { success: boolean; pagination: QwairyPagination; answers: QwairyAnswer[]; } interface QwairySearchResponse { success: boolean; pagination: QwairyPagination; searches: QwairySearchQuery[]; } interface QwairyPerceptionScores { sentiment: number | null; alignment: number | null; consistency: number | null; factualAlignment: number | null; } interface QwairyPerceptionResponse { success: boolean; current: { snapshotId: string; month: number; year: number; scores: QwairyPerceptionScores } | null; previous: { snapshotId: string; month: number; year: number; scores: QwairyPerceptionScores } | null; trends: QwairyPerceptionScores; averages: QwairyPerceptionScores; } interface QwairyArticle { id: string; title: string; slug: string | null; articleType: string; status: 'DRAFT' | 'GENERATING' | 'LIVE' | 'ARCHIVED'; wordCount: number; finalUrl: string | null; publishedAt: string | null; } interface QwairyContentResponse { success: boolean; pagination: QwairyPagination; articles: QwairyArticle[]; } interface QwairyTechnicalAnalysisResponse { success: boolean; analyzed: boolean; aiReadiness: { score: number; issuesCount: number; optimizationsCount: number } | null; robotsAnalysis: Record | null; llmsAnalysis: Record | null; sitemapAnalysis: Record | null; lastAnalyzedAt: string | null; } ``` *** ## Pagination Fetch all results when you have more than 100 items. ```javascript JavaScript theme={null} async function fetchAllCompetitors(client, brandId, period = 30) { const allCompetitors = []; let offset = 0; const limit = 100; while (true) { const response = await client.getCompetitors(brandId, { period, limit, offset }); allCompetitors.push(...response.competitors); if (response.competitors.length < limit || allCompetitors.length >= response.pagination.total) { break; } offset += limit; } return allCompetitors; } // Usage const allCompetitors = await fetchAllCompetitors(client, 'your-brand-id', 30); console.log(`Total competitors: ${allCompetitors.length}`); ``` ```python Python theme={null} def fetch_all_competitors(client, brand_id: str, period: int = 30) -> list: """Fetch all competitors with pagination.""" all_competitors = [] offset = 0 limit = 100 while True: response = client.get_competitors(brand_id, period=period, limit=limit, offset=offset) all_competitors.extend(response['competitors']) if len(response['competitors']) < limit or len(all_competitors) >= response['pagination']['total']: break offset += limit return all_competitors # Usage all_competitors = fetch_all_competitors(client, 'your-brand-id', period=30) print(f"Total competitors: {len(all_competitors)}") ``` *** ## Field Reference Mapping between business metrics and API fields: | Business Metric | Endpoint | Field | Description | | ------------------------------- | --------------------- | ---------------------------------- | ------------------------------------------------------------ | | Mention Rate | `/performance` | `scores.mentionRate` | % of brand-mentioning responses that mention yours | | Citation Rate | `/performance` | `scores.sourceRate` | % of source-citing responses that cite your domain | | Coverage | `/performance` | `scores.coverage` | % of all monitored responses mentioning your brand | | Share of Voice | `/performance` | `scores.shareOfVoice` | Your % of all brand mentions (SELF + DIRECT) | | Share of Voice (per competitor) | `/competitors` | `shareOfVoice` | A competitor's % of all brand mentions | | Share of Citations | `/source-domains` | `rate` | Source's % of all citations | | Sentiment | `/performance` | `scores.sentiment` | Average sentiment score (0-100) | | Average Position | `/competitors` | `avgPosition` | Average rank in AI responses (1 = first) | | Mentioned but not cited | `/answers` | `hasSelfMention` + `hasSelfSource` | Responses where you're mentioned but not linked | | AI search queries | `/search` | `query` | Web queries triggered by AI before responding | | Perception scores | `/perception` | `current.scores` | Sentiment, alignment, consistency, factual alignment (0-100) | | Content articles | `/content` | `articles` | Content Studio articles (Markdown via `/content/{id}`) | | AI Readiness Score | `/technical-analysis` | `aiReadiness.score` | robots/llms/sitemap readiness (0-100) | Use `/performance` for aggregated KPIs. Use `/competitors` and `/source-domains` for detailed breakdowns. Use `/answers` and `/search` for LLM-level diagnostics. Use `/perception`, `/content` and `/technical-analysis` for reputation, content and technical readiness. # Weekly Reports Source: https://docs.qwairy.co/developers/guides/weekly-reports Generate automated week-over-week performance comparison reports Automate weekly GEO performance reports with trend analysis and week-over-week comparisons. This guide uses the API client from the [Guides index](/developers/guides/index#api-client). Copy it to your project first. ## What You'll Build A weekly report showing: * **Current vs previous week** metrics comparison * **Trend indicators** (up/down/stable) * **Percentage change** for each KPI *** ## Generate Weekly Report ```javascript JavaScript theme={null} async function generateWeeklyReport(client, brandId) { const now = new Date(); // Calculate date ranges const thisWeekEnd = now.toISOString().split('T')[0]; const thisWeekStart = new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; const lastWeekEnd = thisWeekStart; const lastWeekStart = new Date(now - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; // Fetch both periods in parallel const [thisWeek, lastWeek] = await Promise.all([ client.getPerformance(brandId, { startDate: thisWeekStart, endDate: thisWeekEnd }), client.getPerformance(brandId, { startDate: lastWeekStart, endDate: lastWeekEnd }), ]); const calculateChange = (current, previous) => { if (!previous || previous === 0) return null; return ((current - previous) / previous * 100).toFixed(1); }; const getTrend = (current, previous) => { if (current > previous) return 'up'; if (current < previous) return 'down'; return 'stable'; }; const metrics = [ { name: 'Mention Rate', key: 'mentionRate' }, { name: 'Source Rate', key: 'sourceRate' }, { name: 'Coverage', key: 'coverage' }, { name: 'Share of Voice', key: 'shareOfVoice' }, { name: 'Sentiment', key: 'sentiment' }, ]; return { generatedAt: new Date().toISOString(), period: { thisWeek: { start: thisWeekStart, end: thisWeekEnd }, lastWeek: { start: lastWeekStart, end: lastWeekEnd }, }, metrics: metrics.map(m => ({ name: m.name, current: thisWeek.scores[m.key], previous: lastWeek.scores[m.key], change: calculateChange(thisWeek.scores[m.key], lastWeek.scores[m.key]), trend: getTrend(thisWeek.scores[m.key], lastWeek.scores[m.key]), })), methodology: { thisWeek: thisWeek.methodology.responsesTotal, lastWeek: lastWeek.methodology.responsesTotal, }, }; } ``` ```python Python theme={null} from datetime import datetime, timedelta from typing import Optional def generate_weekly_report(client, brand_id: str) -> dict: """Generate a week-over-week comparison report.""" now = datetime.now() # Calculate date ranges this_week_end = now.strftime('%Y-%m-%d') this_week_start = (now - timedelta(days=7)).strftime('%Y-%m-%d') last_week_end = this_week_start last_week_start = (now - timedelta(days=14)).strftime('%Y-%m-%d') # Fetch both periods this_week = client.get_performance(brand_id, start_date=this_week_start, end_date=this_week_end) last_week = client.get_performance(brand_id, start_date=last_week_start, end_date=last_week_end) def calculate_change(current: float, previous: float) -> Optional[float]: if not previous or previous == 0: return None return round((current - previous) / previous * 100, 1) def get_trend(current: float, previous: float) -> str: if current > previous: return 'up' if current < previous: return 'down' return 'stable' metrics = [ ('Mention Rate', 'mentionRate'), ('Source Rate', 'sourceRate'), ('Coverage', 'coverage'), ('Share of Voice', 'shareOfVoice'), ('Sentiment', 'sentiment'), ] return { 'generated_at': datetime.now().isoformat(), 'period': { 'this_week': {'start': this_week_start, 'end': this_week_end}, 'last_week': {'start': last_week_start, 'end': last_week_end}, }, 'metrics': [ { 'name': name, 'current': this_week['scores'][key], 'previous': last_week['scores'][key], 'change': calculate_change(this_week['scores'][key], last_week['scores'][key]), 'trend': get_trend(this_week['scores'][key], last_week['scores'][key]), } for name, key in metrics ], 'methodology': { 'this_week': this_week['methodology']['responsesTotal'], 'last_week': last_week['methodology']['responsesTotal'], }, } ``` *** ## Usage ```javascript JavaScript theme={null} const client = new QwairyClient(process.env.QWAIRY_API_TOKEN); const report = await generateWeeklyReport(client, 'your-brand-id'); // Console output console.log(`\n Weekly GEO Report: ${report.period.thisWeek.start} to ${report.period.thisWeek.end}\n`); console.log('Metric Current Previous Change'); console.log('─'.repeat(55)); for (const m of report.metrics) { const trend = m.trend === 'up' ? '↑' : m.trend === 'down' ? '↓' : '→'; const changeStr = m.change !== null ? `${m.change > 0 ? '+' : ''}${m.change}%` : 'N/A'; console.log(`${m.name.padEnd(20)} ${String(m.current).padEnd(10)} ${String(m.previous).padEnd(11)} ${trend} ${changeStr}`); } ``` ```python Python theme={null} client = QwairyClient() report = generate_weekly_report(client, 'your-brand-id') print(f"\n Weekly GEO Report: {report['period']['this_week']['start']} to {report['period']['this_week']['end']}\n") print(f"{'Metric':<20} {'Current':<10} {'Previous':<11} {'Change':<10}") print('─' * 55) for m in report['metrics']: trend = '↑' if m['trend'] == 'up' else '↓' if m['trend'] == 'down' else '→' change_str = f"{'+' if m['change'] and m['change'] > 0 else ''}{m['change']}%" if m['change'] is not None else 'N/A' print(f"{m['name']:<20} {m['current']:<10} {m['previous']:<11} {trend} {change_str}") ``` *** ## Example Output **Console:** ``` Weekly GEO Report: 2024-12-12 to 2024-12-19 Metric Current Previous Change ─────────────────────────────────────────────────────── Mention Rate 45.2 42.1 ↑ +7.4% Source Rate 23.9 25.0 ↓ -4.4% Coverage 33.33 31.0 ↑ +7.5% Share of Voice 8.13 7.5 ↑ +8.4% Sentiment 78.1 76.2 ↑ +2.5% ``` **JSON:** ```json theme={null} { "generatedAt": "2024-12-19T10:30:00.000Z", "period": { "thisWeek": { "start": "2024-12-12", "end": "2024-12-19" }, "lastWeek": { "start": "2024-12-05", "end": "2024-12-12" } }, "metrics": [ { "name": "Mention Rate", "current": 45.2, "previous": 42.1, "change": "7.4", "trend": "up" }, { "name": "Source Rate", "current": 23.9, "previous": 25.0, "change": "-4.4", "trend": "down" }, { "name": "Coverage", "current": 33.33, "previous": 31.0, "change": "7.5", "trend": "up" }, { "name": "Share of Voice", "current": 8.13, "previous": 7.5, "change": "8.4", "trend": "up" }, { "name": "Sentiment", "current": 78.1, "previous": 76.2, "change": "2.5", "trend": "up" } ] } ``` *** ## Add Diagnostic Drivers (Optional) Enrich your report with the "why" behind metric changes: which providers moved, which prompts gained or lost citations. ```javascript JavaScript theme={null} async function getReportDrivers(client, brandId, startDate, endDate) { const [answers, searchQueries] = await Promise.all([ client.getAnswers(brandId, { startDate, endDate, hasSelfMention: true, hasSelfSource: false, limit: 5, sort: 'createdAt', order: 'desc', }), client.getSearch(brandId, { startDate, endDate, limit: 5, sort: 'createdAt', order: 'desc', }), ]); return { missedCitations: { count: answers.pagination.total, top: answers.answers.map(a => ({ prompt: a.promptText, provider: a.provider, position: a.selfMentionPosition, })), }, searchQueries: { count: searchQueries.pagination.total, top: searchQueries.searches.map(s => ({ query: s.query, prompt: s.prompt, })), }, }; } // Usage: add to your report const drivers = await getReportDrivers(client, brandId, thisWeekStart, thisWeekEnd); console.log(`\nMissed citations this week: ${drivers.missedCitations.count}`); console.log('Top prompts where you are mentioned but not cited:'); drivers.missedCitations.top.forEach(d => console.log(` - [${d.provider}] "${d.prompt}"`)); ``` ```python Python theme={null} def get_report_drivers(client, brand_id: str, start_date: str, end_date: str) -> dict: """Get diagnostic drivers for the weekly report.""" from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=2) as executor: ans_future = executor.submit( client.get_answers, brand_id, has_self_mention=True, has_self_source=False, limit=5, ) search_future = executor.submit( client.get_search, brand_id, limit=5, ) answers = ans_future.result() search_queries = search_future.result() return { 'missed_citations': { 'count': answers['pagination']['total'], 'top': [ {'prompt': a['promptText'], 'provider': a['provider'], 'position': a.get('selfMentionPosition')} for a in answers['answers'] ], }, 'search_queries': { 'count': search_queries['pagination']['total'], 'top': [ {'query': s['query'], 'prompt': s['prompt']} for s in search_queries['searches'] ], }, } # Usage drivers = get_report_drivers(client, 'your-brand-id', this_week_start, this_week_end) print(f"\nMissed citations this week: {drivers['missed_citations']['count']}") for d in drivers['missed_citations']['top']: print(f" - [{d['provider']}] \"{d['prompt']}\"") ``` *** ## Send Report Deliver the report via Slack or email. ```javascript Slack (JS) theme={null} async function sendToSlack(report, webhookUrl) { const formatMetric = (m) => { const icon = m.trend === 'up' ? ':chart_with_upwards_trend:' : m.trend === 'down' ? ':chart_with_downwards_trend:' : ':arrow_right:'; const sign = m.change > 0 ? '+' : ''; return `${icon} *${m.name}*: ${m.current} (${sign}${m.change}%)`; }; const blocks = [ { type: 'header', text: { type: 'plain_text', text: `Weekly GEO Report` }, }, { type: 'section', text: { type: 'mrkdwn', text: `*Period:* ${report.period.thisWeek.start} to ${report.period.thisWeek.end}`, }, }, { type: 'section', text: { type: 'mrkdwn', text: report.metrics.map(formatMetric).join('\n'), }, }, ]; await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ blocks }), }); } // Usage await sendToSlack(report, process.env.SLACK_WEBHOOK_URL); ``` ```javascript Email (JS) theme={null} const { Resend } = require('resend'); async function sendByEmail(report, to) { const resend = new Resend(process.env.RESEND_API_KEY); const formatRow = (m) => { const arrow = m.trend === 'up' ? '↑' : m.trend === 'down' ? '↓' : '→'; const sign = m.change > 0 ? '+' : ''; return `${m.name}${m.current}${m.previous}${arrow} ${sign}${m.change}%`; }; const html = `

Weekly GEO Report

Period: ${report.period.thisWeek.start} to ${report.period.thisWeek.end}

${report.metrics.map(formatRow).join('')}
MetricCurrentPreviousChange
`; await resend.emails.send({ from: 'reports@yourdomain.com', to, subject: `Weekly GEO Report - ${report.period.thisWeek.end}`, html, }); } // Usage await sendByEmail(report, ['team@company.com']); ``` ```python Slack (Python) theme={null} import requests def send_to_slack(report: dict, webhook_url: str): """Send report to Slack channel.""" def format_metric(m): icon = ':chart_with_upwards_trend:' if m['trend'] == 'up' else ':chart_with_downwards_trend:' if m['trend'] == 'down' else ':arrow_right:' sign = '+' if m['change'] and float(m['change']) > 0 else '' return f"{icon} *{m['name']}*: {m['current']} ({sign}{m['change']}%)" blocks = [ {'type': 'header', 'text': {'type': 'plain_text', 'text': 'Weekly GEO Report'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': f"*Period:* {report['period']['this_week']['start']} to {report['period']['this_week']['end']}"}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '\n'.join(format_metric(m) for m in report['metrics'])}}, ] requests.post(webhook_url, json={'blocks': blocks}) # Usage send_to_slack(report, os.environ['SLACK_WEBHOOK_URL']) ``` ```python Email (Python) theme={null} import resend def send_by_email(report: dict, to: list): """Send report via email using Resend.""" resend.api_key = os.environ['RESEND_API_KEY'] def format_row(m): arrow = '↑' if m['trend'] == 'up' else '↓' if m['trend'] == 'down' else '→' sign = '+' if m['change'] and float(m['change']) > 0 else '' return f"{m['name']}{m['current']}{m['previous']}{arrow} {sign}{m['change']}%" html = f"""

Weekly GEO Report

Period: {report['period']['this_week']['start']} to {report['period']['this_week']['end']}

{''.join(format_row(m) for m in report['metrics'])}
MetricCurrentPreviousChange
""" resend.Emails.send({ 'from': 'reports@yourdomain.com', 'to': to, 'subject': f"Weekly GEO Report - {report['period']['this_week']['end']}", 'html': html, }) # Usage send_by_email(report, ['team@company.com']) ```
*** ## Scheduling Run this report automatically: | Platform | Method | | ------------------ | ------------------------------------- | | **Cron** | `0 9 * * MON` (every Monday at 9am) | | **GitHub Actions** | `schedule: cron: '0 9 * * 1'` | | **AWS Lambda** | EventBridge rule with cron expression | | **Google Cloud** | Cloud Scheduler + Cloud Functions | *** ## Next Steps * Build a [custom dashboard](/developers/guides/custom-dashboard) for real-time monitoring * Add [competitive analysis](/developers/guides/competitive-analysis) to your reports * [Export data](/developers/guides/data-export) to your BI tools # Introduction Source: https://docs.qwairy.co/developers/introduction Get started with the Qwairy API v1 and integrate GEO data into your workflows Build custom dashboards, integrate with your marketing stack, or create automated reports using your GEO (Generative Engine Optimization) data. The API is available for **Growth**, **Business**, and **Enterprise** plans. [Upgrade your plan](https://qwairy.co/dashboard/team/subscription) to get access. ## Base URL All API endpoints are relative to: ``` https://www.qwairy.co/api/v1 ``` ## Available Endpoints The API provides access to all your GEO monitoring data: | Resource | Description | | ----------------------------------------------------------- | -------------------------------------------------------- | | [Brands](/developers/endpoints/brands/list) | List all brands in your account | | [Performance](/developers/endpoints/performance) | Get performance snapshots (Audit FLASH format) | | [Competitors](/developers/endpoints/competitors/list) | Track competitor mentions and share of voice | | [Source Domains](/developers/endpoints/source-domains/list) | Monitor domains cited by AI platforms | | [Source URLs](/developers/endpoints/source-urls) | Track individual pages cited in AI responses | | [Prompts](/developers/endpoints/prompts/list) | Access your monitored prompts/queries | | [Answers](/developers/endpoints/answers/list) | Retrieve AI-generated responses | | [Search](/developers/endpoints/search) | Discover web queries AI models trigger before responding | ## Quick Start Go to **Team Management** → **API Access** and create a new API token. ```bash theme={null} curl -X GET "https://www.qwairy.co/api/v1/brands" \ -H "Authorization: Bearer qw-api-your-token-here" ``` Use the brand ID to access performance metrics, competitors, sources, and more. ## Response Format All responses are JSON formatted with a consistent structure: ```json theme={null} { "success": true, "pagination": { "total": 156, "count": 10, "limit": 10, "offset": 0 }, "prompts": [ ... ] } ``` Most list endpoints include a `pagination` object with: * `total`: Total items matching the query * `count`: Items in this response * `limit`: Max items per page * `offset`: Items skipped The `/brands` endpoint is the exception: it returns the full list with a top-level `total` count and no `pagination` object. See [Entities](/developers/entities) for detailed data structures. Error responses include machine-readable codes: ```json theme={null} { "error": { "code": "BRAND_NOT_FOUND", "message": "Brand not found or not accessible" } } ``` See [Error Codes](/developers/errors) for the complete list. ## Need Help? * Check the [Authentication](/developers/authentication) guide * Review [Rate Limits](/developers/rate-limits) * See [Error Codes](/developers/errors) * Contact us at [team@qwairy.co](mailto:team@qwairy.co) # Rate Limits Source: https://docs.qwairy.co/developers/rate-limits Understand API rate limits and how to handle them Rate limits ensure fair usage and platform stability. Limits are applied **per team**, across all API tokens belonging to that team. ## Current Limits Two windows are enforced simultaneously on every request: | Window | Limit | | ------------------- | ----------------------- | | Per-minute (burst) | 1,000 requests / minute | | Per-day (daily cap) | 20,000 requests / day | A request is accepted only if **both** windows have budget remaining. If either window is exhausted, the API returns `429 Too Many Requests`. Need higher limits for production workloads (e.g. ingesting data for many brands/domains)? Contact us at [team@qwairy.co](mailto:team@qwairy.co). ## Rate Limit Headers Every response, including `429`s, includes headers for both windows: | Header | Description | | ----------------------------- | ------------------------------------------------------------------------------------ | | `X-RateLimit-Limit` | Per-minute request cap | | `X-RateLimit-Remaining` | Requests remaining in the current minute window | | `X-RateLimit-Reset` | ISO 8601 timestamp when the minute window resets | | `X-RateLimit-Daily-Limit` | Per-day request cap | | `X-RateLimit-Daily-Remaining` | Requests remaining in the current day window | | `X-RateLimit-Daily-Reset` | ISO 8601 timestamp when the day window resets | | `Retry-After` | Seconds to wait before retrying, based on the limit actually hit (only set on `429`) | ### Example Headers ```http theme={null} X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 873 X-RateLimit-Reset: 2026-04-07T09:13:00.000Z X-RateLimit-Daily-Limit: 20000 X-RateLimit-Daily-Remaining: 4211 X-RateLimit-Daily-Reset: 2026-04-08T00:00:00.000Z ``` **Always check both windows.** A request can be rejected while `X-RateLimit-Remaining` still shows plenty of per-minute budget if the daily cap is exhausted. Conversely, a healthy daily budget doesn't guarantee the next request will pass the per-minute window. ## Handling Rate Limits When either window is exceeded, the API returns `429 Too Many Requests` with a body indicating which window was hit: ```json theme={null} { "error": "Too Many Requests", "limit": "daily", "message": "Daily rate limit (20000/day) exceeded. Please try again in 32451 seconds.", "retryAfter": 32451 } ``` The `limit` field is either `"burst"` (per-minute) or `"daily"`. `Retry-After` and the `X-RateLimit-*-Reset` headers are always aligned with the actual blocker, so you can trust `Retry-After` without having to inspect the payload. ### Best Practices Use the `Retry-After` header as-is. It already reflects whichever window (minute or day) is the real blocker. Monitor `X-RateLimit-Remaining` and `X-RateLimit-Daily-Remaining` to throttle proactively. Store and reuse data that doesn't change frequently (brands, competitors, topics). Use query filters (`provider`, `period`, `limit`) to fetch only the data you need. ### Retry Logic Example ```javascript theme={null} async function fetchWithRetry(url, options, maxRetries = 10) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status !== 429) return response; // Trust the Retry-After header — it points at the real blocker // (minute or day window), not just the minute reset. const retryAfterSec = Number(response.headers.get('Retry-After') ?? '60'); const waitMs = Math.max(retryAfterSec * 1000, 1000); const body = await response.clone().json().catch(() => ({})); console.warn( `Rate limited on ${body.limit ?? 'unknown'} window. Retrying in ${waitMs}ms...` ); await new Promise((r) => setTimeout(r, waitMs)); } throw new Error('Max retries exceeded'); } ``` If you see `"limit": "daily"` on retry, waiting within the same process rarely helps: the daily window can be hours away. Persist the `X-RateLimit-Daily-Reset` timestamp and reschedule the job instead of blocking a worker. ## Optimizing API Usage ### Use Filtering Parameters Instead of fetching all data and filtering client-side, use query parameters: ```bash theme={null} # Bad: Fetch all answers, filter in code GET /api/v1/brands/{brandId}/answers # Good: Filter at the API level GET /api/v1/brands/{brandId}/answers?provider=chatgpt&hasSelfMention=true&limit=50 ``` ### Cache Static Data Some data changes infrequently and can be cached: * **Brands list**: Changes rarely * **Competitors list**: Changes when you add/remove competitors * **Topics/Tags**: Changes when you modify your workspace ### Use Date Ranges Limit data retrieval to relevant time periods: ```bash theme={null} # Fetch only last 7 days instead of default 30 GET /api/v1/brands/{brandId}/performance?period=7 ``` ## Need Higher Limits? If 20,000 requests per day is too tight for your ingestion pipeline, contact us at [team@qwairy.co](mailto:team@qwairy.co) with your use case and expected volume. We can raise the daily cap on your team. # Outbound Webhooks Source: https://docs.qwairy.co/developers/webhooks Receive Qwairy notification events (weekly report, export ready, low credits) as signed JSON POSTs to your own endpoint. Instead of receiving Qwairy emails, a white-label team can have notifications POSTed to its own endpoint as signed JSON events — so you deliver them to your users under your own brand, from your own systems. When a webhook is enabled, the matching Qwairy email is **not** sent. Outbound webhooks are part of [Agencies](/agencies/introduction), an **Enterprise** capability. The controls are **OWNER**-only. ## Events A workspace's weekly report. A data export has finished and is ready to download. The team is projected to run out of credits before renewal. A `webhook.test` event is also sent by the **Send test event** button so you can validate your endpoint before turning delivery on. ## Set It Up Go to **Team Management > Notifications** and find **Outbound webhooks**. Enter a public `https` URL (private addresses and `http` are rejected) and save. A **signing secret** is generated and shown **once** — copy and store it now. Click **Send test event** to confirm Qwairy can reach your endpoint and your signature check passes. Toggle **Deliver via webhook** on. From now on, the events above go to your endpoint instead of being emailed. You can rotate the secret at any time with **Regenerate secret** (the new value is shown once), and remove the webhook to fall back to email. ## Payload Every request is a `POST` with a JSON envelope: ```json theme={null} { "event": "credits.low", "id": "8b40e94f-79f4-40cc-8d38-bde11ef0d8d9", "createdAt": "2026-06-18T09:00:00.000Z", "team": { "id": "team_abc123", "name": "Acme" }, "data": { "creditsRemaining": 120, "renewalDate": "2026-07-01T00:00:00.000Z" } } ``` The `data` object is event-specific. Treat it as forward-compatible: new fields may be added, so ignore the ones you don't use rather than rejecting the payload. ## Headers | Header | Value | | -------------------- | ----------------------------------------------- | | `X-Qwairy-Event` | the event name, e.g. `credits.low` | | `X-Qwairy-Timestamp` | Unix time (seconds) when the request was signed | | `X-Qwairy-Signature` | `sha256=` HMAC of the request | ## Verifying the Signature The signature is an HMAC-SHA256, keyed with your signing secret, over the timestamp and the **raw** request body joined with a dot — `.` — using the raw bytes, before any JSON parsing. ```js theme={null} import crypto from 'crypto'; function isValidQwairyWebhook(req, secret) { const signature = req.headers['x-qwairy-signature']; // "sha256=" const timestamp = req.headers['x-qwairy-timestamp']; // Reject stale requests to prevent replay (e.g. older than 5 minutes). if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(`${timestamp}.${req.rawBody}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } ``` Verify the signature on every request and reject anything that fails. Use a constant-time comparison, and reject timestamps outside a short window to block replays. ## Delivery Behavior * Qwairy expects a `2xx` response. Non-`2xx`, timeouts, and redirects count as failures. * For the weekly report and low-credit events, a failed delivery is retried on the next scheduled run; the matching email is **not** sent in its place. * The export-ready event is one-shot: if delivery fails, the export still appears in your workspace's **Exports** hub. # Backlink Opportunities Source: https://docs.qwairy.co/documentation/act/backlink-opportunities Discover and leverage backlink opportunities for better SEO Backlinks remain a core pillar of visibility in both traditional SEO and AI-driven indexing. With Qwairy, you can uncover and activate high-value backlink opportunities powered by LLMs' real-world usage.