# Adobe Experience Manager Source: https://docs.tryprofound.com/agent-analytics/adobe_experience_manager This documentation explains how to set up Adobe Experience Manager as a Cloud Service (AEMaaCS) Log Forwarding to deliver CDN logs to the Profound Agent Analytics platform. **This integration is in BETA.** Behavior, supported fields, and configuration steps may change as we onboard the first cohort of customers. If you hit any issues, please contact [support@tryprofound.com](mailto:support@tryprofound.com). ## Overview The integration uses Adobe Experience Manager as a Cloud Service's self-serve HTTPS Log Forwarding to deliver CDN logs to our Agent Analytics API. AEM's CDN is Fastly-backed and streams logs continuously over HTTPS to the destination configured in your program. For more information about AEM Log Forwarding, [visit the Adobe documentation](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/developing/log-forwarding). ## Prerequisites * An AEM as a Cloud Service program with self-serve Log Forwarding enabled (sandbox-tier programs are not supported by Adobe and cannot forward logs) * Cloud Manager access to commit to the program's Git repository and run a Config Pipeline * A Profound Log Ingestion Token for Agent Analytics ## Configuration In Cloud Manager, add your Profound Log Ingestion Token as a secret variable on the environment(s) you want to forward logs from. Navigate to **Configuration → Environment Variables → Add** and configure: * **Name** — `PROFOUND_LOG_INGESTION_TOKEN` * **Type** — `Secret` * **Service Applied** — `All` * **Value** — Your provisioned Log Ingestion Token for Profound's Agent Analytics platform (`bot_PROFOUND_LOG_INGESTION_TOKEN`) Add a `logForwarding.yaml` file under a top-level `config/` directory in your program's Git repository. This is the folder structure required by AEM's Config Pipeline. ```yaml config/logForwarding.yaml theme={null} kind: "LogForwarding" version: "1" metadata: envTypes: ["prod"] data: https: default: enabled: true url: "https://artemis.api.tryprofound.com/v1/logs/aem" authHeaderName: "X-API-Key" authHeaderValue: "${{PROFOUND_LOG_INGESTION_TOKEN}}" aem: enabled: false ``` `aem.enabled: false` disables forwarding of AEM application, Apache, and Dispatcher log families — only CDN logs are sent to Profound. `metadata.envTypes` scopes the configuration to specific environment types. Set it to `["prod"]` to forward only production CDN logs, or include `"stage"` and `"dev"` to forward those too. Omit the `metadata` block to apply to every environment the Config Pipeline targets. AEM CDN logs do not include a referer field by default. To populate Profound's `referer` field, add a CDN request transformation that copies the incoming `Referer` header into a custom log property. Add the following to your program's CDN configuration file (for example, `config/cdn.yaml`): ```yaml config/cdn.yaml theme={null} kind: "CDN" version: "1" data: requestTransformations: rules: - name: profound-log-referer when: "*" actions: - type: set logProperty: referer value: reqHeader: referer ``` The `logProperty` name must be exactly lowercase `referer`. Variants like `Referer` or `referrer` will not be recognized and the field will silently remain empty. Deploy the configuration with a Cloud Manager **Config Pipeline** (Targeted Deployment). `logForwarding.yaml` and CDN config files are deployed only by a Config Pipeline. Full Stack and Web Tier pipelines do not deploy these files. 1. In Cloud Manager, go to **Pipelines → Add Pipeline → Targeted Deployment → Config Pipeline**. 2. Point the pipeline at the branch holding `config/logForwarding.yaml` (and `config/cdn.yaml` if you configured one). 3. Target the environment(s) where log forwarding should be active. 4. Run the pipeline. On first delivery, AEM's underlying Fastly delivery layer issues a one-time HTTP challenge to `/.well-known/fastly/logging/challenge` to validate the destination. Profound's endpoint handles this automatically — no action required on your side. If you inspect Cloud Manager logs and see a single challenge request, this is expected. That's it! AEM will now send CDN logs to Profound. Data should begin appearing in your dashboard within a few minutes. ## What gets captured AEM CDN log entries are mapped to Profound's visit schema as follows: * Time and IP * `timestamp` — Time the CDN received the request. * `cli_ip` — Client IP address. * Request * `host` — Host header from the client request. * `url` — Path and query string requested. * `method` — HTTP method. * `req_ua` — User-Agent header reported by the client. * `referer` — Referer header (only populated when the optional CDN request transformation in Step 3 is deployed). * Response * `status` — HTTP response status code. * `ttfb` — Time to first byte, in milliseconds. * Response body size — Adobe's CDN log schema does not emit a native response-size field, so this is reported as `0` during BETA. ## Troubleshooting * If logs aren't appearing, confirm the pipeline that deployed was a **Config Pipeline** — not Full Stack or Web Tier * If the pipeline succeeds but no requests reach Profound, the program may still be on Adobe's legacy support-ticket log forwarding. File a ticket with Adobe to migrate to self-serve Log Forwarding before retrying * If volume looks roughly 2× expected, legacy and self-serve log forwarding may be running in parallel during a migration window — Adobe typically resolves this within a few hours * For `401` or `403` responses, verify `PROFOUND_LOG_INGESTION_TOKEN` is set with **Service Applied: All** and that the token is active in Profound * Confirm the `url` begins with `https://` — AEM rejects the configuration at deploy time otherwise * Ensure `aem.enabled: false` is present so non-CDN log families are not forwarded ## Additional Resources * [Adobe — Log Forwarding for AEM as a Cloud Service](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/developing/log-forwarding) * [Adobe — CDN log entry format](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/developing/logging#cdn-log) * [Adobe — Use Config Pipelines](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/config-pipeline) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions ## Security Considerations * Store the Log Ingestion Token as a Cloud Manager secret variable, never as a plaintext environment value * Regularly rotate your Log Ingestion Token * Monitor log forwarding activity for unusual patterns # Akamai Datastream2 Integration Source: https://docs.tryprofound.com/agent-analytics/akamai This documentation explains how to set up Akamai Datastream2 to forward your CDN logs to the Profound Agent Analytics platform. ## Overview The integration uses Akamai Datastream2 to forward your CDN logs to our Agent Analytics API. Akamai Datastream2 is a service that enables reliable delivery of streaming data to various destinations including HTTP endpoints. For more information about Akamai Datastream2, [visit the Akamai documentation](https://techdocs.akamai.com/datastream2/docs/welcome-datastream2). ## Prerequisites * An Akamai account with Datastream2 add-on * Access to your Akamai Datastream2 configuration * A Profound Log Ingestion Token for Agent Analytics ## Configuration Akamai Detected Sign in to Akamai Control Center and navigate to [DataStream](https://control.akamai.com/apps/data-stream-ui/#/streams/group/all) in the left sidebar. Click `Create a stream` DataStream create stream Navigation Select your property and click `Next` DataStream property selection Select the following options in each section: **Log Information** log information **Message Exchange Data** message data **Request Header Data** request header data **Network Data** network data **Log Format** log format Read more about these fields in the [Akamai documentation](https://techdocs.akamai.com/datastream2/docs/data-set-parameters) Add authentication information: * From the **Destination** dropdown, select **Custom HTTPS**. * Select **BASIC** as the **Authentication** type. * Enter `https://artemis.api.tryprofound.com/v1/logs/akamai` as the **URL**. * Enter your organization name in the **Username** field. * Enter your Log Ingestion Token in the **Password** field. * Check **Send compressed data**. authentication Select **JSON** as the **Log format**. output format Choose **30** seconds as the push frequency. push frequency Review the configuration. It should look like the following: Review configuration When ready, click `Save stream`. DataStream2 takes approximately **60 minutes** to start sending logs. Please wait for **60 minutes** before checking for logs. After creating the stream, you will now be instructed to activate the stream. activate stream Click `Proceed to Property Manager` and select the property you created the stream for. Click **Edit a new version** to add the stream to the property. Navigate to the **Property Configuration Settings**. Select **Standard property behavior** from the `+ Behavior` dropdown. add behavior Search for **DataStream** and click `Insert Behavior` datastream behavior The behavior will show **DataStream 1**. Ignore this as you will select version 2 later. Scroll down to the **DataStream** section and select: * **DataStream2** from the **Stream Version** dropdown. * Toggle **Enable** to **On**. * Select the stream you created in the **Stream Name**. * Select a sample rate. **100%** is recommended. datastream settings Select **Log Delivery** from the left sidebar. Enable: * **Log Host Header** * **Log Referer Header** * **Log User-Agent Header** Leave the other settings as their default values. log delivery settings Save the configuration and activate the new version. That's it! Akamai will now send real-time logs to Profound. Data should begin appearing in your dashboard within **60 minutes** of activating the stream. ## Troubleshooting * Ensure your stream is activated, connected to the property, and that the property has been updated * Ensure your Log Ingestion Token is correct ## Additional Resources * [Akamai Datastream2 Documentation](https://techdocs.akamai.com/datastream2/docs/welcome-datastream2) * [Creating a Stream Documentation](https://techdocs.akamai.com/datastream2/docs/create-stream) * [Data Set Parameters Documentation](https://techdocs.akamai.com/datastream2/docs/data-set-parameters) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions ## Security Considerations * Regularly rotate your Log Ingestion Token * Monitor your log drain usage for unusual patterns or unexpected traffic spikes # Amazon CloudFront Integration (Data Firehose) Source: https://docs.tryprofound.com/agent-analytics/aws_firehose_cloudfront This documentation explains how to set up Amazon CloudFront real-time logs delivery to Profound Agent Analytics platform using Amazon Data Firehose. ## Overview The integration uses Amazon Data Firehose to forward CloudFront real-time logs to our Agent Analytics API. Amazon Data Firehose is an AWS service that enables reliable delivery of streaming data to various destinations including HTTP endpoints. For more information about CloudFront real-time logs, [visit the AWS documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/real-time-logs.html). ## Prerequisites * An AWS Account with CloudFront and Amazon Data Firehose permissions * Access to your CloudFront distribution configuration * A Profound Log Ingestion Token for Agent Analytics ## Configuration Cloudfront Detected Sign in to the AWS Console and navigate to the Amazon Data Firehose console Firehose Console Navigation Create a new delivery stream, select "Direct PUT" as source and "HTTP Endpoint" as destination Firehose HTTP Destination 1. Configure the HTTP endpoint with the following URL format: ```http theme={null} https://artemis.api.tryprofound.com/v1/logs/aws_data_firehose_cloudfront ``` 2. For authentication, provide your Profound Log Ingestion Token as the access key (we recommend using AWS Secrets Manager for secure token storage). If you are using the [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html), please use the following [JSON format](https://docs.aws.amazon.com/firehose/latest/dev/secrets-manager-whats-secret.html) when creating the secret: ```json theme={null} { "api_key": "bot_PROFOUND_LOG_INGESTION_TOKEN" } ``` 3. Enable GZIP content encoding. Firehose Destination Settings 4. Create a new S3 bucket to store failed delivery logs (required by AWS) Firehose S3 Destination Go to your CloudFront distribution and navigate to the "Logging" tab. CloudFront Logging Tab Click the "Add" button and select "Kinesis Data Firehose" as the destination. (Kinesis Data Firehose is the legacy name for Amazon Data Firehose) CloudFront Logging Add Now you should be in the "Add standard logging destination" screen. Select the delivery stream you created in Step 3. CloudFront Logging Select Under "Additional settings - optional", select the following fields: CloudFront Logging Additional Settings * Time and IP * `date` - Date when the request was completed * `time` - Time when the request was completed * `c-ip` - Client IP address * Request Details * `cs-method` - HTTP request method * `x-host-header` - Host header value of the request * `cs-uri-stem` - Request URI path * `cs-uri-query` - Request query string * `cs(User-Agent)` - Client user agent * `cs(Referer)` - Request referrer * Response Details * `sc-status` - HTTP response status * `sc-bytes` - Response size in bytes * `time-taken` - Request processing time Select `JSON` as the **Output format**. Output format Click **Submit** to save the configuration. That's it! CloudFront will now send real-time logs to Data Firehose, which forwards them to Profound. Data should begin appearing in your dashboard within a few minutes. ## Troubleshooting * If logs aren't appearing, verify your Firehose delivery stream status in CloudWatch * Check Data Firehose monitoring for delivery errors * Ensure your Log Ingestion Token is correct * Verify CloudFront real-time logging is enabled for your distribution * Check IAM roles have proper permissions for both CloudFront and Firehose ## Additional Resources * [Amazon CloudFront Real-time Logs Documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/real-time-logs.html) * [Amazon Kinesis Data Firehose Documentation](https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions ## Security Considerations * Store Log Ingestion Tokens in AWS Secrets Manager * Regularly rotate your Log Ingestion Token * Monitor CloudWatch logs for unusual patterns * Enable CloudTrail for API activity monitoring * Use IAM roles with least privilege access # Cloudflare Integration (Logpush) Source: https://docs.tryprofound.com/agent-analytics/cloudflare_logpush This documentation explains how to set up and deploy a Cloudflare Worker that captures HTTP request data and forwards it to Profound Agent Analytics platform using Cloudflare Logpush. ## Overview The integration uses a Cloudflare Logpush that runs as middleware to collect request metadata and send it to our Agent Analytics API. Cloudflare Logpush is a Cloudflare Enterprise feature that allows you to forward Cloudflare logs to a non-Cloudflare destination like Profound. For more information related to Cloudflare Logpush, [visit the documentation site](https://developers.cloudflare.com/logs/about/). ## Prerequisites * A Cloudflare account with Enterprise plan * Access to your domain's Cloudflare configuration * A Profound Log Ingestion Token for Agent Analytics Using Cloudflare Free/Pro plan? Check out our integration guide using Cloudflare Workers. Logpush Detected ## Configuration Select your domain on Cloudflare Dashboard, go to **Analytics & Logs -> Logpush** Cloudflare Logpush Navigation Create a new Logpush job, select HTTP destination Cloudflare HTTP Destination Use the following API URL format to include your Profound Log Ingestion Token and continue ```http theme={null} https://artemis.api.tryprofound.com/v1/logs/cloudflare_logpush?header_X-API-Key=bot_PROFOUND_LOG_INGESTION_TOKEN ``` Select `Filtered logs` under **If logs match...** And filter by the `ClientRequestHost` field to match your domain Ensure you use your domain in the filter. Do not use `www.tryprofound.com`, this is only an example. Cloudflare Filter Logs Select HTTP requests dataset, and choose the following fields to send: * Request * `ClientIP` - IP Address of the client. * `ClientRequestHost` - Host requested by the client. * `ClientRequestMethod` - HTTP method of client request. * `ClientRequestReferer` - HTTP request referrer. * `ClientRequestURI` - URI requested by the client. * `ClientRequestUserAgent` - User agent reported by the client * Performance * `EdgeStartTimestamp` - Timestamp at which the edge received request from the client. * `EdgeEndTimestamp` - Timestamp at which the edge finished sending response to the client. * Response * `EdgeResponseBytes` - Number of bytes returned by the edge to the client. * `EdgeResponseStatus` - HTTP status code returned by Cloudflare to the client. That's it! You have now successfully configured Profound Agent Analytics for your website. Data should begin to populate on your dashboard within an hour. ## Troubleshooting * If logs aren't appearing, verify your Log Ingestion Token and URL * Check Workers > Analytics for any execution errors * Ensure your route pattern matches your domain configuration * Verify the Worker is receiving requests by checking the Cloudflare dashboard metrics ## Additional Resources * [Cloudflare Logpush Documentation](https://developers.cloudflare.com/logs/about/) * [Cloudflare Enable HTTP destination Documentation](https://developers.cloudflare.com/logs/get-started/enable-destinations/http/) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions ## Security Considerations * Store Log Ingestion Tokens as secrets in production environments * Regularly rotate Log Ingestion Tokens * Monitor Worker usage and logs for unusual patterns # Cloudflare Integration (Worker) Source: https://docs.tryprofound.com/agent-analytics/cloudflare_worker This documentation explains how to set up and deploy a Cloudflare Worker that captures HTTP request data and forwards it to Profound Agent Analytics platform using Cloudflare Worker. ## Overview The integration uses a Cloudflare Worker that runs as middleware to collect request metadata and send it to our Agent Analytics API. The Worker captures important request information like IP addresses, user agents, and referrers without affecting the actual request handling. ## Prerequisites * A Cloudflare account with access to Workers * Node.js installed on your development machine * Access to your domain's Cloudflare configuration * A Profound Log Ingestion Token for Agent Analytics Using Cloudflare Enterprise plan? Check out our integration guide using Cloudflare Logpush. Logpush is always preferable to a Worker: it is out-of-band and cannot affect live traffic. A Worker on a `*` route sits in the request path for **every** request to that hostname, so it can affect production traffic. Use the code below as-is, and in particular: * Never clone the response or read the response body inside the Worker. Buffering bodies can exceed the Worker's 128 MB isolate memory limit, which Cloudflare surfaces to the visitor (or crawler) as [Error 1102 `Worker exceeded resource limits`](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1102/). * Keep the logging call inside `ctx.waitUntil()` and swallow its errors so a slow or failing log request can never turn into a 5xx. * Keep sensitive paths out of the Worker entirely. The path list in the code below only skips logging; requests still pass through the Worker. To take checkout, cart, and admin paths out of the Worker's path completely, narrow the route pattern itself (or add a higher-priority route that is not bound to the Worker). Symptoms of a Worker that breaks these rules are intermittent 5xx responses on large or streamed responses, and downstream fallout such as Google Merchant Center / Google Ads disapprovals ("destination not working", "image not accessible") when `AdsBot-Google` hits one of those errors during a crawl. Running a Shopify storefront? Do not put a Worker in front of it. Use one of the log drain options in our Shopify guide instead, which collects logs out-of-band. Logpush Detected ## Implementation Guide Create a new Worker project and install dependencies: ### Create a new Worker project First, use the Cloudflare Worker CLI to create a new Worker project: In this example, we're using version `2.37.4` of the Cloudflare Worker CLI. You may use the latest version, but there might be some minor process differences. ```bash theme={null} npm create cloudflare@2.37.4 -- log-collector ``` After npm launches, you'll be prompted to select a starting point. Select "Hello World" as your starting category. Cloudflare Worker Step 1 - Choose Starting Point Select the "Hello World" worker template. Cloudflare Worker Step 1 - Select Worker Template Select the "TypeScript" language. Cloudflare Worker Step 1 - Select Language Now the CLI will create a new project with the name `log-collector` and install the necessary dependencies. Select Git for version control. Cloudflare Worker Step 2 - Select Version Control Deploy the application now. The application will be deployed with a cloudflare development domain and will not affect your production environment. Cloudflare Worker Step 3 - Deploy Application Cloudflare CLI will prompt you to login and select the account you want to deploy the application to. Please choose the account your domain is associated with. Cloudflare should automatically open a browser window with "Hello World" displayed. You can navigate to the project directory once the application is deployed. ```bash theme={null} cd log-collector ``` Edit your `wrangler.json` file to configure the `PROFOUND_API_URL` environment variable and the route binding: Replace pattern `example.com/*` with your actual domain. Use your target site URL (usually marketing site). For example, if your marketing site is `https://www.example.com`, you should use `www.example.com/*` as the pattern. The `zone_name` should be your canonical domain without the [www](http://www). If you are not sure about the correct configuration, please contact [Profound support](mailto:support@tryprofound.com). ```json wrangler.json theme={null} { "$schema": "node_modules/wrangler/config-schema.json", "name": "log-collector", "main": "src/index.ts", "compatibility_date": "2025-01-29", "observability": { "enabled": true }, "route": { "pattern": "example.com/*", "zone_name": "example.com" }, "vars": { "PROFOUND_API_URL": "https://artemis.api.tryprofound.com/v1/logs/cloudflare_worker" } } ``` Then copy the TypeScript code into `src/index.ts`: ```typescript src/index.ts theme={null} /** * Cloudflare Worker for Log Collection * * Forwards request metadata to Profound's log collection API. The origin * response body is never read or modified, and every error in the logging * path is swallowed, so logging cannot alter the response. */ export interface Env { PROFOUND_API_URL: string; PROFOUND_LOG_INGESTION_TOKEN: string; } // Paths that are not logged. Matched on full path segments, so '/cart' does // not match '/cartography'. const EXCLUDED_PATHS = ['/checkout', '/cart', '/admin', '/api']; // Percent-decoded and lowercased so encoded variants such as '/%63heckout' // and '/checkout%2Fpayment' match the same way the origin routes them. function normalizePath(pathname: string): string { try { return decodeURIComponent(pathname).toLowerCase(); } catch { return pathname.toLowerCase(); } } function isExcluded(pathname: string): boolean { const path = normalizePath(pathname); return EXCLUDED_PATHS.some( (excluded) => path === excluded || path.startsWith(`${excluded}/`), ); } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const response = await fetch(request); const skip = response.status === 101 || isExcluded(new URL(request.url).pathname); if (!skip) { ctx.waitUntil( sendLog(request, response, env).catch((error) => console.error('Failed to send logs:', error), ), ); } return response; }, } satisfies ExportedHandler; async function sendLog(request: Request, response: Response, env: Env) { const requestUrl = new URL(request.url); // +2 for ': ' and +2 for '\r\n' per header const headerSize = Array.from(response.headers.entries()).reduce( (total, [key, value]) => total + key.length + value.length + 4, 0, ); // Derived from content-length so the body is never buffered. Streamed or // chunked responses have no content-length and are reported as headers only. // HEAD and bodyless statuses advertise a content-length that is never sent, // so their body size is counted as zero. const contentLength = Number(response.headers.get('content-length')); const transmitsBody = request.method !== 'HEAD' && response.status !== 204 && response.status !== 304; const bodySize = transmitsBody && Number.isFinite(contentLength) ? contentLength : 0; const bytes = headerSize + bodySize; const logData = { timestamp: Date.now(), host: requestUrl.hostname, method: request.method, pathname: requestUrl.pathname, query_params: Object.fromEntries(requestUrl.searchParams), ip: request.headers.get('cf-connecting-ip'), userAgent: request.headers.get('user-agent'), referer: request.headers.get('referer'), bytes, status: response.status, }; const logResponse = await fetch(env.PROFOUND_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': env.PROFOUND_LOG_INGESTION_TOKEN, }, body: JSON.stringify([logData]), signal: AbortSignal.timeout(5000), }); await logResponse.body?.cancel(); } ``` `bytes` is derived from the `content-length` response header instead of the response body. Reading the body (for example via `response.clone()` and `blob()`) forces Cloudflare to buffer the whole payload in the Worker isolate, which fails with a 5xx on large or streamed responses. This Worker never reads or modifies the response body — it streams the origin response through unchanged — but it is still in the request path, so origin fetch failures surface as they would without it. `bytes` therefore counts headers plus `content-length`, and only when the response actually transmits a body (not `HEAD`, `204`, or `304`). ### Login to Cloudflare Use the Wrangler CLI to deploy the Worker: ```bash theme={null} # Login to Cloudflare npx wrangler login ``` ### Configure the Profound Log Ingestion Token Secrets is a feature of Cloudflare Workers that allows you to store sensitive information like Log Ingestion Tokens in a secure environment. ```bash theme={null} # Configure the Log Ingestion Token secret npx wrangler secret put PROFOUND_LOG_INGESTION_TOKEN ``` You will be prompted to enter the Log Ingestion Token. Copy and paste the token and press enter. Configure Log Ingestion Token ### Deploy the Worker ```bash theme={null} # Deploy the Worker npx wrangler deploy ``` Verify your Worker is functioning correctly: Navigate to Profound Analytics and check if the logs are being collected in the Log panel. Note that AI log filter is on by default. Please disable it using the filter on the top right corner of the Logs panel. If logs are appearing, you are all set! You should be able to see data populating in the Analytics dashboard. ## Troubleshooting * If logs aren't appearing, verify your `PROFOUND_API_URL` environment variable and `PROFOUND_LOG_INGESTION_TOKEN` secret are configured correctly * Check Cloudflare Workers > Analytics for any execution errors * Ensure your route pattern matches your domain configuration * Verify the Worker is receiving requests by checking the Cloudflare dashboard metrics * Intermittent 5xx after deploying the Worker: check Metrics > Errors > Invocation Statuses for `Exceeded Memory` (Error 1102) or `Script threw exception` (Error 1101). Both mean the Worker itself is failing, not your origin. Confirm you are running the code above (no `clone()`, no body reads) and that the logging call is wrapped in `ctx.waitUntil()` with a `.catch()` * Google Ads / Merchant Center disapprovals such as "destination not working" or "image not accessible" after deploying the Worker: these come from `AdsBot-Google` or the Merchant Center fetcher receiving one of the Worker errors above. Fix the Worker errors, then request a re-review * To rule the Worker out entirely, remove the route binding (`npx wrangler triggers delete` or delete the route in the dashboard) and confirm the errors stop ## Additional Resources * [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) * [Wrangler CLI Documentation](https://developers.cloudflare.com/workers/wrangler/) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions ## Security Considerations * Store Log Ingestion Tokens as secrets in production environments * Regularly rotate Log Ingestion Tokens * Monitor Worker usage and logs for unusual patterns # Crawler Verification Source: https://docs.tryprofound.com/agent-analytics/crawler-verification How we verify AI and search engine crawlers ## Crawler Verification Process Understanding who's accessing your website is crucial for security and analytics accuracy. Profound Agent Analytics platform employs robust verification methods to ensure that crawlers claiming to be from major AI and search platforms are genuine. ## Why Verification Matters Accurate crawler identification is essential for: * Protecting your website from malicious actors * Ensuring data accuracy in your analytics * Managing resource allocation effectively * Maintaining security compliance * Optimizing content delivery for legitimate AI platforms ## Verification Methods ### Primary Verification Techniques Our platform employs multiple verification methods to ensure accuracy: 1. **Reverse DNS Lookup** * Verifies the crawler's hostname matches the claimed organization * Provides an additional layer of authenticity checking * Used by established platforms like Google and Apple 2. **IP Range Validation** * Confirms the crawler originates from the organization's known IP ranges * Particularly effective for platforms like OpenAI and You.com * Updated regularly to maintain accuracy 3. **Heuristic Detection** * Analyzes crawler behavior patterns * Identifies characteristic signatures * Helps verify crawlers without published verification methods ## Platform-Specific Verification ### Fully Verified Platforms * [**Google**](https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers) * Google-CloudVertexBot: Reverse DNS verification * Googlebot: Reverse DNS verification + ASN verification * Google-Extended: Reverse DNS verification * Storebot-Google: Reverse DNS verification * Googlebot-News: Reverse DNS verification * Googlebot-Video: Reverse DNS verification * Googlebot-Image: Reverse DNS verification * Google-NotebookLM: ASN verification * Google-Agent: Reverse DNS verification + IP range verification + ASN verification * GoogleAgent-Mariner: IP range verification * Google-Other: Heuristic detection only * [**Microsoft Bing**](https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0) * BingBot: Reverse DNS verification * MicrosoftPreview: Reverse DNS verification * [**Apple**](https://support.apple.com/en-us/119829) * Applebot: Reverse DNS verification + IP range verification * Applebot-Extended: Reverse DNS verification + IP range verification * [**OpenAI**](https://platform.openai.com/docs/bots) * OAI-SearchBot: IP range verification + ASN verification * ChatGPT-User: IP range verification + ASN verification * GPTBot: IP range verification + ASN verification * OAI-Operator: ASN verification * [**Duck Duck Go**](https://duckduckgo.com/duckduckgo-help-pages/results/duckduckbot) * DuckDuckBot: IP range verification * DuckAssistBot: ASN verification * [**Perplexity**](https://docs.perplexity.ai/guides/bots) * PerplexityBot: IP range verification * Perplexity-User: IP range verification * [**Meta**](https://developers.facebook.com/docs/sharing/webmasters/web-crawlers/#identify-2) * Meta-ExternalAgent: ASN verification * Meta-ExternalFetcher: ASN verification * Meta-WebIndexer: ASN verification * FacebookBot: Heuristic detection only * [**Amazon**](https://developer.amazon.com/amazonbot) * AmazonBot: Reverse DNS verification + IP range verification * Amazon-SearchBot: IP range verification * Amazon-User: IP range verification * AmazonBuyForMe: ASN verification ### Partially Verified Platforms Some platforms use common cloud provider IPs or don't publish verification methods, making complete verification challenging: * [**Anthropic**](https://privacy.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler) * ClaudeBot: ASN + Heuristic detection * Claude-User: ASN + Heuristic detection * Claude-SearchBot: ASN + Heuristic detection * **You.com** * YouBot: Reverse DNS verification + IP range verification * **Bytedance** * Bytespider: Reverse DNS verification * TikTokSpider: Heuristic detection only * [**Common Crawl**](https://commoncrawl.org/ccbot) * CCBot: IP range verification * **Yahoo** * Slurp: Reverse DNS verification * **OpenClaw** * OpenClaw: Heuristic detection only * **DeepSeek** * DeepSeek: Reverse DNS verification * **Baidu** * BaiduSpider: Reverse DNS verification * **Huawei** * PetalBot: ASN verification * [**Mistral**](https://docs.mistral.ai/robots) * MistralAI-User: IP range verification * **Yandex** * YandexBot: IP range verification * **Gemini** * Google: Heuristic detection * Gemini-Deep-Research: Reverse DNS verification + ASN verification ## Stay Updated Our platform continuously updates verification methods as: * New AI platforms emerge * Existing platforms modify their crawler infrastructure * Additional verification methods become available * Security requirements evolve ## Important Note About Data Updates We continuously monitor and improve our verification processes as the AI crawler landscape evolves. As we enhance our detection methods and crawler identification techniques, you may notice changes in your historical and current analytics data. These updates reflect our commitment to providing the most accurate and reliable crawler identification possible. If you observe any significant changes in your data, it's likely due to improvements in our verification system. We recommend regularly reviewing your analytics dashboard to stay informed about the latest insights into AI crawler behavior on your site. # Custom Log Integration Source: https://docs.tryprofound.com/agent-analytics/custom This documentation explains how to send your application logs directly to Profound's analytics platform using our Custom Log Integration API. ## Overview The Custom Log Integration allows you to send your application logs directly to Profound's analytics platform using a standardized JSON format. This integration supports batch processing of logs and includes robust validation to ensure data quality while providing detailed feedback on any validation issues. ## Prerequisites * Your Profound Log Ingestion Token * The ability to send HTTP POST requests from your application/infrastructure ## API Specification ### Endpoint ```http theme={null} POST https://artemis.api.tryprofound.com/v1/logs/custom ``` ### Authentication Include your Profound Log Ingestion Token in the request header: ```http theme={null} 'x-api-key': 'bot_PROFOUND_LOG_INGESTION_TOKEN' ``` ### Request Format Logs must be sent as an array of JSON objects. Each log entry requires specific fields: #### Required Fields * `timestamp` - Event timestamp (Unix timestamp or ISO 8601 string) * `method` - HTTP method (max 10 characters) * `host` - Request hostname (max 255 characters) * `path` - Request path (max 2048 characters) * `status_code` - HTTP status code (range: 100-599) * `ip` - Client IP address (max 45 characters) * `user_agent` - User agent string (max 1024 characters) #### Optional Fields Both `query_params` and `referer` are optional. However, if you do not provide either, human referral data will **not be available** in your dashboard. * `query_params` - Query parameters (key max: 100 chars, value max: 1000 chars) * `referer` - Request referer (max 2048 characters) * `bytes_sent` - Response size in bytes (must be >= 0) * `duration_ms` - Request duration in milliseconds (must be >= 0) Each request can contain up to 1,000 log entries. For larger datasets, split your logs into multiple requests. ## Implementation Guide Custom Detected Prepare your log data in the required JSON format: ```json theme={null} [ { "timestamp": "2024-01-11T12:00:00Z", "method": "GET", "host": "example.com", "path": "/products", "status_code": 200, "ip": "192.168.1.1", "user_agent": "Mozilla/5.0...", "query_params": { "category": "electronics", "page": "1" }, "referer": "https://example.com", "bytes_sent": 1024, "duration_ms": 150 } ] ``` Here's how to send logs using popular programming languages: Python: ```python theme={null} import requests import json logs = [{ "timestamp": "2024-01-11T12:00:00Z", "method": "GET", "host": "example.com", "path": "/products", "status_code": 200, "ip": "192.168.1.1", "user_agent": "Mozilla/5.0..." }] response = requests.post( "https://artemis.api.tryprofound.com/v1/logs/custom", headers={ "x-api-key": "bot_PROFOUND_LOG_INGESTION_TOKEN", "Content-Type": "application/json" }, json=logs ) print(response.json()) ``` Node.js: ```javascript theme={null} const axios = require('axios'); const logs = [{ timestamp: "2024-01-11T12:00:00Z", method: "GET", host: "example.com", path: "/products", status_code: 200, ip: "192.168.1.1", user_agent: "Mozilla/5.0..." }]; axios.post('https://artemis.api.tryprofound.com/v1/logs/custom', logs, { headers: { 'x-api-key': 'bot_PROFOUND_LOG_INGESTION_TOKEN', 'Content-Type': 'application/json' } }) .then(response => console.log(response.data)) .catch(error => console.error(error)); ``` Process the API response to confirm successful ingestion or handle any errors: Successful Response: ```json theme={null} { "status": "accepted", "message": "Processing X log entries", "first_visit_id": "uuid-string", "errors": [] } ``` Response with Validation Errors: ```json theme={null} { "status": "accepted", "message": "Processing 98 log entries", "first_visit_id": "uuid-string", "errors": [ "Invalid log format: timestamp out of reasonable range", "Invalid log format: status_code must be between 100 and 599" ] } ``` ## Error Handling The API handles errors in two ways: ### Validation Errors For expected validation issues (invalid timestamps, malformed data, etc.), the API will: * Continue processing valid entries in the batch * Add validation errors to the `errors` array in the response * Return a 200 status code with processed entries and errors ### Unexpected Errors For unexpected errors (server issues, database problems, etc.), the API will: * Return a 500 status code * Return an error message in the response detail field * Not process any entries in the batch ## Best Practices * Group logs into batches of up to 1,000 entries * Implement retry logic for failed requests * Use background processing for log submission * Send logs asynchronously to avoid impact on application performance * Consider implementing local buffering * Use compression for large payloads ## Support * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for assistance ## Security Considerations * Store Log Ingestion Tokens securely * Regularly rotate Log Ingestion Tokens * Monitor request logs for unusual patterns * Use HTTPS for all API requests *** # Adobe Analytics Extension Source: https://docs.tryprofound.com/agent-analytics/extensions/adobe-analytics Connect Adobe Analytics with Profound Agent Analytics to correlate AI-referred traffic with human engagement, orders, and revenue ## Overview Profound Agent Analytics now offers seamless integration with Adobe Analytics, enhancing your ability to correlate AI interactions with human engagement and business outcomes. This integration bridges the gap between AI visibility and traditional web analytics, providing a comprehensive view of your digital performance ecosystem. By connecting your Adobe Analytics report suite, Agent Analytics can import critical engagement and revenue metrics alongside our specialized AI tracking capabilities. This integration works both as a complementary extension to our core platform or as a standalone solution, giving you flexibility in how you monitor and optimize your digital presence for both human and AI visitors. The Adobe Analytics integration automatically synchronizes your analytics data in real-time when accessing the Adobe Analytics dashboard, allowing you to make data-driven decisions with the most current information available while maintaining strict privacy controls. ### Adobe Analytics Metrics The following metrics are retrieved from the [Adobe Analytics Reporting API](https://developer.adobe.com/analytics-apis/docs/2.0/guides/endpoints/reports/): * **Referrer** (`referrer`): The referring source of the visit, used to identify traffic originating from AI systems * **Visits** (`visits`): The number of human sessions referred by AI platforms * **Orders** (`orders`): Completed purchase events from AI-referred traffic * **Revenue** (`revenue`): Total monetary value generated from AI-referred visits ### Dashboard Once connected, the Adobe Analytics tab in your Profound dashboard provides a unified view of how AI-referred traffic converts on your website. #### Summary Metrics At the top of the dashboard, you'll see high-level metrics for your selected date range: * **Humans visited from AI systems**: The total number of human visitors who arrived at your site after being referred by AI assistants * **Revenue from AI systems**: The total revenue attributed to AI-referred traffic Both metrics include period-over-period comparison to help you track trends. #### Platforms The Platforms table breaks down AI-referred traffic by the AI system that sent the visit. Each platform row includes: * **Visits**: Number of human visits referred by the platform * **Orders**: Completed purchase events from those visits * **Revenue**: Total revenue generated from AI-referred visitors * **Conversion Rate**: Percentage of visits that resulted in an order #### Human Visits by Page The page-level breakdown shows which pages humans visit most after being referred by AI assistants. Each row includes: * **Visits**: Number of AI-referred visits to the page * **Orders**: Purchase events that occurred on or through the page * **Revenue**: Revenue attributed to the page ## Prerequisites * Adobe Analytics account * Adobe Analytics configured for your website * Access to a target Report Suite via your Adobe account ## Configuration within Adobe Admin Console Go to the [Adobe Admin Console](https://adminconsole.adobe.com/) and select your Adobe Analytics product. Adobe Admin Console In the **Product Profiles** section, create a new profile for Profound's Agent Analytics Integration using the **New Profile** button. You'll be prompted to give it a name and optionally a description. New Profile Click into the new product profile and navigate to the **Permissions** tab. Product Profile Permissions Click on the edit icon within the **Report Suites** row. Edit Icon For **Report Suites**, **Metrics**, and **Dimensions**, toggle on **Auto-include** for each one. Auto-include Within the **Report Suite Tools** tab, add the **Report Suite Mgmt** permission. Report Suite Tools Within the **Analytics Tools** tab, add the **Web Services** and **Web Service Access** permissions. Analytics Tools Permissions Click **Save**. ## Configuration within Adobe Developer Console Navigate to the [Adobe Developer Console](https://developer.adobe.com/) and click **Create new project**. Make sure you are creating the project under the same organization that the product profile was created under in the Admin Console. Create New Project Select **Add API**. Add API Select **Adobe Analytics**. Select Adobe Analytics Select **Server-to-Server Authentication** and click **Next**. After clicking next, input what you'd like to name the credential (e.g. Profound Agent Analytics) and then click **Next** again. Server-to-Server Authentication From the **Product Profile** list, select the product profile that was created in the Admin Console in the previous steps. Then click **Save configured API** on the bottom right. Select Product Profile In the newly created project, navigate to the **OAuth Server-to-Server** tab and retrieve the **Client ID**, **Client Secret**, and **Organization ID**. These values will be necessary when connecting the project within Profound. OAuth Credentials ## Configuration within Profound Learn how to integrate Adobe Analytics with your existing Agent Analytics configuration. Navigate to the **Adobe Analytics** tab. Here you will find a button to **Connect account**. Adobe Analytics Tab Click **Connect account**. Set up Adobe Analytics Input the **Client Secret**, **Client ID**, and **Organization ID** retrieved from the Adobe Developer Console from the previous steps. Credentials Input Once your Adobe account is connected, choose your Report Suite. Select a report suite to connect to this domain from the dropdown. Choose Report Suite Click **Complete setup** to finalize the connection. Complete Setup That's it! Adobe Analytics is now connected. Reload your Adobe Analytics tab to see data from your selected report suite. Learn how to integrate Adobe Analytics without Agent Analytics Navigate to the **Adobe Analytics Setup** tab. Here you will find a button to **Connect account**. Adobe Analytics Tab Click **Connect account**. Set up Adobe Analytics Input the **Client Secret**, **Client ID**, and **Organization ID** retrieved from the Adobe Developer Console from the previous steps. Credentials Input Once your Adobe account is connected, choose your Report Suite. Select a report suite to connect to this domain from the dropdown. Choose Report Suite Click **Complete setup** to finalize the connection. Complete Setup That's it! Adobe Analytics is now connected. Reload your Adobe Analytics tab to see revenue data from your selected report suite. ## Disconnecting You can disconnect the Adobe Analytics integration at any time in the Settings tab. Your integration will be disabled immediately. ## Security and Privacy Profound requests READ ONLY access to your Adobe Analytics data and does not download, save, or otherwise retain your Adobe Analytics data. Your data is fetched on demand for processing and display purposes only. We persist minimal report suite information (ID, name, and association with your selected website) to maintain the connection until you choose to disconnect the Adobe Analytics integration. You can opt out at any time through the Settings tab, giving you complete control over your analytics data. ## Considerations * Only one report suite can be connected per website at this time * Only one member of your organization must complete the Adobe authentication process. They must have access to the target Adobe Analytics account and report suites. * Adobe Analytics can be blocked by various browsers or browser extensions. For best results, we recommend using Adobe Analytics in conjunction with the Agent Analytics platform ## Additional Resources * [Adobe Analytics API Documentation](https://developer.adobe.com/analytics-apis/docs/2.0/) * [Adobe Exchange Listing](https://exchange.adobe.com/apps/ec/600242/adobe-analytics-profound-agent-analytics-integration) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions # Google Analytics (GA4) Extension Source: https://docs.tryprofound.com/agent-analytics/extensions/google-analytics Connect AI visibility with human engagement metrics to create a complete picture of your digital performance and revenue impact ## Overview Profound Agent Analytics now offers seamless integration with Google Analytics 4 (GA4), enhancing your ability to correlate AI interactions with human engagement and business outcomes. This integration bridges the gap between AI visibility and traditional web analytics, providing a comprehensive view of your digital performance ecosystem. By connecting your GA4 property, Agent Analytics can import critical conversion and revenue metrics alongside our specialized AI tracking capabilities. This integration works both as a complementary extension to our core platform or as a standalone solution, giving you flexibility in how you monitor and optimize your digital presence for both human and AI visitors. The GA4 integration automatically synchronizes your analytics data in real-time when accessing the Google Analytics dashboard, allowing you to make data-driven decisions with the most current information available while maintaining strict privacy controls. ### GA4 Metrics * Sessions (`sessions`): Track human visitor engagement based on GA4 session data * Transactions (`transactions`): Monitor purchase-related events including completed sales and refunds * Purchase Revenue (`purchaseRevenue`): Quantify the financial impact of your digital presence * Session Key Event Rate (`sessionKeyEventRate`): Evaluate conversion efficiency through the percentage of sessions resulting in transactions * Page Path Analysis (`pagePath`): Identify your most valuable content for both humans and AI systems ## Prerequisites * Google Analytics account * Google Analytics configured for your website * Access to a target Property via your Google account ## Configuration Learn how to integrate GA4 with your existing Agent Analytics Configuration. Navigate to the Google Analytics tab. Here you will find a button to Connect Google Analytics. Google Analytics Tab Click **Configure Google Analytics**. This will direct you to connect your google account to Profound. When prompted, *you should select the Google account which has access to the GA4 Project you want to integrate.* Connect Google Account Google OAuth Select the website you want to connect. This will determine which domain displays GA4 data. Then, select the GA4 property to retrieve data from. Select Website and Property Profound will now verify the connection. Configured That's it! Google Analytics is now connected. Reload your Google Analytics tab to see revenue data from your selected property. Learn how to integrate GA4 without Agent Analytics Navigate to Website tab in the sidebar. If you have not configured Agent Analytics, you will see the following window. Referrals Tab Click **Google Analytics** near the bottom. This will direct you to connect your google account to Profound. When prompted, *you should select the Google account which has access to the GA4 Project you want to integrate.* Connect Google Account Google OAuth Select **+ Add a new website** and enter your domain name. Use the domain for which your target GA4 property is configured. Then, select the GA4 property to retrieve data from. Add New Website Profound will now verify the connection. Configured ## Disconnecting You can disconnect GA4 integration at any time in the Settings tab. Your integration will be disabled immediately. Disconnecting ## Security and Privacy Profound requests READ ONLY access to your Google Analytics data and does not download, save, or otherwise retain your Google Analytics data. Your data is fetched on demand for processing and display purposes only. We persist minimal property information (ID, name, and association with your selected website) to maintain the connection until you choose to disconnect the GA4 integration. You can opt out at any time through the Settings tab, giving you complete control over your analytics data. ## Considerations * Only one property can be connected per website at this time * Every team member must complete the Google authentication process. They must have access to the target GA4 project and property * Google Analytics is blocked by various browsers or browser extensions. For best results, we recommend using GA4 in conjunction with the Agent Analytics platform ## Additional Resources * [Google Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1/rest) * [Google Analytics Metric Information](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions # Frequently Asked Questions Source: https://docs.tryprofound.com/agent-analytics/faq ## Agent Analytics Profound supports integrations with many providers. However, managed providers cannot be integrated at this time. Managed provider like those above offer a streamlined experience that abstracts the technical requirements of setting up and hosting a website. This abstraction makes setting up a website much easier, but consequently obfuscates the niche features necessary to integrate Agent Analytics. Generally, Profound requires a means to access your site's server logs to analyze traffic and surface insights about bot activity. In most cases, managed providers simply do not offer such a mechanism. Without the ability to access your site's server logs, we cannot integrate with your site. If you are using a managed provider, please contact your provider to inquire about server logs. If you have some means of accessing your site's server logs, we offer a custom endpoint where these can be sent. Please contact support to learn more. Yes, you will need to have access to your site's hosting platform and permissions to configure the CDN. Profound provides comprehensive documentation for each supported platform, including step-by-step instructions and configuration examples. However, we cannot edit your site's code or CDN configuration for you. Thus, it is necessary to have technical expertise and access to your site's code / CDN to complete the integration. You can verify that the log collection is working by checking the **Logs** tab in **My Website**. After completing the onboarding process, please allow a few minutes for data to begin populating. If there is no data after 24 hours, please contact support. Cloudfront sometimes sends logs from a unique domain that differs from your site's domain. This is normal and expected, but means the key tied to your real site's domain does not match the domain name coming from Cloudfront. Please contact support to alter the domain. Data will begin appearing shortly after the domain is updated. Cloudflare integrations need to be configured at the domain level. This can be accessed by signing into Cloudflare > select an account > **Account Home** (on the left sidebar) > select your domain > complete the steps in the documentation. The domain name at the top of the page is a dropdown menu. Click once to see a list of registered domains, then select the domain you want to view. ## Google Analytics The domain name to enter when registering via Google Analytics can be any label of your choosing. If you are configuring only GA4, use whichever label make sense to you. If you are configuring both GA4 and Agent Analytics for the same site, use the site name you registered with Agent Analytics. This will combine the data to see it all in one place. Google Analytics exposes fundamentally different data than the server logs coming from a CDN integration. Using a CDN integration allows Profound to track traffic on your site at the most granular level. GA4 does not have the same level of granularity in its data. We are able to track human sessions only, including identifying a referral from AI platforms. Additionally, GA4 provides data that is not exposed in the server logs. Namely, we can see transaction and revenue data that is associated with AI referred traffic. If someone on your team has registered a Google Analytics property linked to your Profound account, and you are not able to see any data, please ensure you have registered your Google account as well. For security purposes, we do not share the access tokens from one account to another. If you teammates are able to see data, they have valid Google tokens associated with their account. To see the data from your account, please ensure you have authorized Profound to access your Google account. You can do this in **My Website** → **Settings** → **Connect Google Analytics** at the bottom of the page. If you have just registered Agent Analytics via a CDN integration (i.e. within the last week), Profound has not yet collected enough data to accurately represent your site's traffic. Google Analytics will have historical data available immediately, but Agent Analytics will take some time to catch up. Additionally, regardless of sign up date, GA4 can be blocked by certain browsers or extensions. The Agent Analytics data cannot be blocked. Thus, your GA4 data may be slightly different than the Agent Analytics data. # Fastly HTTPS Endpoint Source: https://docs.tryprofound.com/agent-analytics/fastly This documentation explains how to set up Fastly real-time logs delivery to Profound Agent Analytics platform using Fastly custom HTTPS Endpoint ## Overview The integration uses Fastly's custom HTTPS endpoint to forward Fastly real-time logs to our Agent Analytics API. Fastly's real-time log streaming offers instant visibility into traffic, performance, threats, and troubleshooting, and streams logs to various destinations including HTTP endpoints. For more information about Fastly real-time logs, [visit the Fastly documentation](https://docs.fastly.com/en/guides/streaming-logs). ## Prerequisites * A Fastly account * Access/permission to edit Logging Endpoints * A Profound Log Ingestion Token for Agent Analytics ## Configuration Fastly Detected Sign into the Fastly console, select your service, and navigate to the Service configuration tab. Service Configuration Tab Select "Edit configuration" and navigate to the Logging section. Find "HTTPS" and click `+ Create endpoint` Logging Endpoint Configure your HTTPS endpoint with the following settings: Configuration * **Name:** Add an informative name * **Placement:** Use the default * **URL:** Enter `https://artemis.api.tryprofound.com/v1/logs/fastly` * **Maximum logs & Maximum bytes:** Optional, configure as needed. * **Log format**: Paste the following log format. This governs what information will be sent to the Agent Analytics API. If you use a different format, your logs may be rejected by the API. ``` { "timestamp": "%{strftime(\{"%Y-%m-%dT%H:%M:%S%z"\}, time.start)}V", "client_ip": "%{req.http.Fastly-Client-IP}V", "start_time": "%{begin:%Y-%m-%dT%H:%M:%S%z}t", "end_time": "%{end:%Y-%m-%dT%H:%M:%S%z}t", "host": "%{req.http.Host}V", "url": "%{json.escape(req.url)}V", "request_method": "%{json.escape(req.method)}V", "request_referer": "%{json.escape(req.http.referer)}V", "request_user_agent": "%{json.escape(req.http.User-Agent)}V", "response_status": %{resp.status}V, "response_body_size": %{resp.body_bytes_written}V } ``` * Time and IP * `timestamp` - Time when the request was completed * `client_ip` - Client IP address * Request Details * `request_method` - HTTP request method * `host` - Requested host header * `url` - Request url path * `request_referer` - Request referrer * `request_user_agent` - Client user agent * Response Details * `start_time` - The time at which the request began * `end_time` - The time at which the request finished * `response_status` - HTTP response status * `response_body_size` - Response size in bytes *For more information see the [documentation](https://docs.fastly.com/en/guides/useful-variables-to-log)* You may see a warning like: Warning Message You can safely ignore this message. The validation has been configured for you. Next, click "Attach a condition" Attach a condition 1 Attach a condition 2 Enter the following using your domain name: `req.http.host == "my-website.com"` Filter to host This filter will ensure that only logs for your selected domain are sent to Profound. Select `Save and apply`. Scroll down to "Advanced options" Advanced Options Configure: * **Content type** - `application/json` * **Custom header name** - `X-API-Key` * **Custom header value** - Your provisioned Log Ingestion Token for Profound's Agent Analytics Platform (`bot_PROFOUND_LOG_INGESTION_TOKEN`) * **Method** - `POST` * **JSON log entry format** - `Array of JSON` * **Select a log line format** - `Blank` Save your configuration and activate the new service version. That's it! Fastly will now send real-time logs to Profound. Data should begin appearing in your dashboard within a few minutes. ## Troubleshooting * If logs aren't appearing, verify your HTTPS endpoint configuration * Check fastly for delivery errors * Verify your Log Ingestion Token is correct * Verify your API endpoint is correct ## Additional Resources * [Fastly's real-time log streaming](https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features) * [Log streaming: HTTPS](https://docs.fastly.com/en/guides/log-streaming-https) * [Log Formats](https://docs.fastly.com/en/guides/useful-log-formats) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions # Google Cloud CDN Source: https://docs.tryprofound.com/agent-analytics/gcp_log_sink This documentation explains how to set up Google Cloud CDN log delivery to Profound Agent Analytics platform using a log sink. ## Overview This integration uses a topic, subscription, and log sink to capture Google Cloud CDN logs and forward them to Profound Agent Analytics platform. ## Prerequisites * A GCP account with Google Cloud CDN enabled * Access and edit permissions to the GCP project * API endpoint details for the Profound Agent Analytics platform ## Configuration GCP setup Sign into the GCP Console and navigate to `Topics`, you can search for 'topics' in the search bar. Create a new topic by clicking the `Create Topic` button near the top. Create a topic Give your topic a name and click `Create` Navigate to the `Subscriptions` tab on the left sidebar. Click the `Create Subscription` button near the top. Create subscription 1. Give your subscription a name. 2. Select the topic you created in step 2. 3. Use delivery type `Push` * Enter `https://artemis.api.tryprofound.com/v1/logs/gcp_log_sink` as the endpoint * Enable authentication * Paste the service account this subscription will use into the `Service account` field * Leave `Audience` blank * Enable payload unwrapping Add retention policy configurations at your discretion. Retention policy Click `Add a Transform` button. Enter `profound_agent_analytics_reduce` as the function name. Paste the following code into the function body: ```javascript theme={null} /* This UDF is used to filter the unused fields before sending to Profound. * * @param {Object} message - The message to reduce. * @param {Object} metadata - The metadata of the message. * @returns {Object} The reduced message. */ function profound_agent_analytics_reduce(message, metadata) { // parse the message const data = JSON.parse(message.data); // remove unused fields delete data['insertId']; delete data['jsonPayload']; delete data['logName']; delete data['receiveTimestamp']; delete data['resource']; delete data['severity']; delete data['spanId']; delete data['trace']; message.data = JSON.stringify(data); return message; } ``` This function is used to drop extraneous fields before sending to Profound. This reduces log export volume, improves performance, and enhances data security. Transform function Click `Validate` to ensure the function is valid. Add additional configurations at your discretion. Additional configurations Click `Create` to create the subscription. Navigate to the `Log Router` tab by searching 'log router' in the search bar. Click the `Create sink` button near the top. Give your sink a name and optional description. In the `Sink destination` section, select `Cloud Pub/Sub topic` as the `sink service` . In the `sink destination` field, select the topic you created in step 2. Sink service Add the following filters to your sink in the `Choose logs to include in sink` section. Make sure to replace the placeholders with your real load balancer and url map names. **This is only an example.** ```toml theme={null} resource.type="http_load_balancer" resource.labels.forwarding_rule_name="MY-FORWARDING-RULE-NAME" resource.labels.url_map_name="MY-URL-MAP-NAME" ``` Sink filters Finally, click `Create sink` at the bottom. If you have multiple load balancers and url maps, you can modify the filters to include multiple resources like: ```toml theme={null} resource.type="http_load_balancer" AND ( (resource.labels.forwarding_rule_name="MY-FORWARDING-RULE-NAME" AND resource.labels.url_map_name="MY-URL-MAP-NAME") OR (resource.labels.forwarding_rule_name="MY-FORWARDING-RULE-NAME-2" AND resource.labels.url_map_name="MY-URL-MAP-NAME-2") ) ``` If you are registering multiple GCP domains with Profound, you will need to create a separate service account for each domain. The account is used to decode the JWT token and confirm the source domain. Navigate back to Profound. On the Agent Analytics onboarding page, paste the same service account email you used in step 3 into the `GCP Service Account` field. Ensure you click `Save` after pasting the email. Service account Click `Continue` to complete the onboarding process. That's it! Google Cloud CDN will now send real-time logs to Profound. Data should begin appearing in your dashboard within a few minutes. ## Troubleshooting * Ensure you have shared the same service account email with Profound that you used to create the subscription. * Check the `Logs` tab in Profound to verify the logs are being received. * Verify the service account has the necessary permissions to access the topic as pub/sub. * Verify the topic is created and active. * Verify the subscription is created and active. ## Additional Resources * [Third Party Log Routing Documentation](https://docs.cloud.google.com/logging/docs/export/pubsub#integrate-thru-pubsub) * [Service Account Management Documentation](https://docs.cloud.google.com/iam/docs/create-service-agents#grant-role) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for API-related questions # Netlify Integration Source: https://docs.tryprofound.com/agent-analytics/netlify This guide explains how to configure Netlify Log Drains to forward your application logs to Profound Agent Analytics platform for comprehensive monitoring and analysis. Netlify Log Drains are only available on Enterprise plans. ## Overview Netlify General HTTP Endpoint Log Drains enable you to stream your application logs to external services for enhanced monitoring and analytics. By integrating with Profound's Agent Analytics platform, you can gain deeper insights into your site's performance, track visitor behavior, and monitor AI agent interactions in real-time. ## Prerequisites * A Netlify account with Enterprise plan * Administrative access to your Netlify team settings * Your Profound Log Ingestion Token (provisioned during onboarding) * A deployed Netlify site with production traffic ## Configuration Netlify Detected Navigate to your project in the Netlify dashboard. On the left, click **Logs > Log Drains** Log Drains Click **Enable Log Drain** and select `General HTTP Endpoint` Http Endpoint Prepare your log drain. Uncheck: * **Function logs** * **Edge function logs** * **Deploy logs** * **WAF logs** * **Exclude personally identifiable information (PII)** You should only use **Traffic Logs** for the log drain. Prepare your endpoint and authorization: * URL: `https://artemis.api.tryprofound.com/v1/logs/netlify` * Authorization Header: `Bearer bot_xxx` Leave **JSON** selected in the **Log Drain Format**. Configure Http Endpoint Your configuration should match the screenshot above. Click **Connect** to connect the log drain. If you see the following error: `incorrectly configured GetURL, validation request returns status code 400` double check you have prefixed your token with `Bearer ` Configuration Complete That's it! Netlify will now send real-time logs to Profound. Data should begin appearing in your dashboard within a few minutes. ## Troubleshooting * If logs aren't appearing in your Profound dashboard, verify your endpoint targets `/v1/logs/netlify` * For authentication or Log Ingestion Token issues ensure you have prefixed the token with `Bearer ` ## Additional Resources * [Netlify Log Drains Documentation](https://docs.netlify.com/monitor-sites/log-drains/?monitoring-providers=general) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for integration assistance ## Security Considerations * Store Log Ingestion Tokens securely in environment variables * Regularly rotate your Log Ingestion Token * Monitor log drain activity for unusual patterns * Use HTTPS endpoints for all log forwarding # About Agent Analytics Source: https://docs.tryprofound.com/agent-analytics/overview Gain unprecedented visibility into how AI systems interact with your website ## Overview Profound Agent Analytics provides enterprise-grade visibility into how AI systems interact with your digital presence. As AI search engines and assistants become primary discovery channels, understanding and optimizing for AI visibility is crucial for modern digital strategy. ## The AI Analytics Challenge ### The Rise of AI Web Interactions AI systems are revolutionizing how information is discovered and consumed online. Search engines like ChatGPT, Claude, and Google Gemini don't just index your content – they interpret it, synthesize it, and present it to users in entirely new ways. ### Why Traditional Analytics Fall Short Traditional web analytics tools were designed for human visitors who: * Execute JavaScript * Maintain cookies and sessions * Follow predictable browsing patterns AI systems operate differently: * They bypass JavaScript execution * Don't maintain session state * Access content through specialized crawlers * Exhibit varying behavioral patterns based on their purpose (indexing, training, or real-time assistance) This fundamental mismatch makes traditional tracking methods ineffective for monitoring AI interactions. ## The Profound Solution ### Architecture Built for AI Profound's analytics platform is engineered specifically for tracking AI systems: * **Server-Side First**: Implementation at the infrastructure level ensures complete visibility of AI interactions * **Edge Integration**: Direct CDN integration captures requests before caching, providing the most accurate data * **Advanced Bot Detection**: Sophisticated algorithms differentiate between various types of AI crawlers * **High-Performance Backend**: Columnar OLAP databases enable real-time analysis of billions of requests ### Comprehensive AI Visibility Track interactions from major AI platforms: * OpenAI (ChatGPT) * Anthropic (Claude) * Google (Gemini/VertexAI) * Perplexity * Microsoft (Bing) * Apple * Meta * DeepSeek * Emerging AI systems ### Real-Time Intelligence Monitor AI interactions as they happen: * Track which content AI systems access * Understand how your content appears in AI responses * Identify content gaps and optimization opportunities * Analyze AI crawling patterns and frequencies ## Implementation Approaches ### CDN Integration For websites using Content Delivery Networks, we recommend integrating at the CDN level: * **Maximum Accuracy**: Capture requests before caching occurs * **Complete Coverage**: Track all AI interactions, including cached content * **Simplified Setup**: Native integration with major providers: ### Roadmap Roadmap timelines are estimates and subject to change as Profound continues to expand its integration coverage. #### 2025 * Cloudflare (Q1 2025) * Vercel (Q1 2025) * Amazon CloudFront (Q1 2025) * Fastly (Q2 2025) * Netlify (Q2 2025) * Akamai (Q3 2025) * Google Cloud CDN (Q4 2025) * WordPress (Q4 2025) #### 2026 * Shopify (Q1 2026) * Amazon Amplify (Q1 2026) * General AWS S3 Bucket Integration (Q3 2026) * Microsoft Azure CDN (Q4 2026) ### Direct Server Integration For applications without CDN infrastructure: * **API Integration**: Direct integration with your application servers * **Log Ingestion**: Process server logs for AI interaction data * **Custom Solutions**: Tailored implementations for unique infrastructures ## Key Features ### AI Crawler Analytics * Detailed crawler identification and classification * Real-time tracking of crawl events * Pattern analysis and trend monitoring * Coverage analysis and gap detection ### Performance Metrics * Response time analysis * Content accessibility metrics * Cache effectiveness monitoring * Geographic distribution of AI access ### Content Visibility * Page-level AI interaction tracking * Optimization recommendations * Content effectiveness scoring ### Advanced Reporting * Trend analysis * Export capabilities ## Technology Stack Profound is built on enterprise-grade technology: * **Detection Engine**: Advanced machine learning models for accurate bot classification * **Data Processing**: High-performance columnar databases for real-time analytics * **API Layer**: RESTful APIs for seamless integration * **Security**: Enterprise-grade encryption and access controls ## Getting Started Ready to gain visibility into your AI presence? Step-by-step guides for implementing Profound Analytics Connect with our integration specialists # Page type methodology Source: https://docs.tryprofound.com/agent-analytics/page-type-methodology How Profound classifies website pages for Agent Analytics and benchmarking. Profound classifies website URLs into page types so Agent Analytics can show how AI traffic and benchmarking performance differ across kinds of content. Page types are used in page-level reporting and benchmarking views. They help compare similar pages against similar pages instead of mixing fundamentally different URL types, such as blog posts, product pages, documentation, pricing pages, and technical infrastructure. ## How page types are assigned Profound classifies URL patterns, not every individual URL independently. For each website, Profound studies the host's URL structure, groups similar paths like `/blog/`, `/docs/`, `/products/*`, or `/pricing`, and assigns durable page types to those reusable patterns. Once patterns are saved, future URLs are tagged deterministically by matching their host and path against the active mappings. When a path is ambiguous or risky, Profound may use page evidence such as the title, meta description, headings, and visible page text to make a second-pass classification decision. This helps avoid relying on URL shape alone. ## Page type definitions | Page type | Definition | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `blog_editorial` | Informational or editorial content such as articles, guides, thought leadership, news, press releases, case studies, glossary pages, research pages, media posts, and SEO content. | | `product_page` | Pages where a user evaluates, discovers, compares, or selects an offering. This includes product, feature, solution, ecommerce product detail, category, marketplace, catalog, listing, inventory, integration, and template pages. | | `docs_support` | Help, support, and documentation pages. This includes technical docs, help center articles, API references, manuals, troubleshooting pages, warranty pages, and support content. | | `landing_page` | Campaign-specific or conversion-focused pages, such as promotional pages, seasonal pages, partner campaigns, paid acquisition pages, product launch pages, event or webinar registration pages, trial pages, sign up pages, and lead-capture pages. | | `brand_about` | Pages about the company, brand, organization, or physical presence. This includes about pages, company pages, mission, team, leadership, careers, contact, locations, store locators, and investor pages. Home, security, and trust pages may classify here when supported by host-specific mappings or page evidence. | | `pricing` | Pricing, plans, subscription tiers, packages, and plan comparison pages. | | `ugc_page` | Public pages created or owned by users, customers, members, creators, vendors, or community participants rather than the brand itself. Examples include public profiles, creator pages, storefronts, vendor profiles, forum threads, community questions, reviews, and donation or tip pages. | | `technical_infra` | Machine-readable or infrastructure URLs used for crawling, indexing, AEO/SEO, APIs, tracking, or site operation. Examples include `robots.txt`, `llms.txt`, sitemaps, manifests, service workers, XML/JSON feeds, API endpoints, tracking endpoints, metrics endpoints, CDN/proxy URLs, and similar infrastructure files. | | `file_asset` | Direct downloadable or static file URLs, such as PDFs, images, videos, ZIP files, spreadsheets, presentations, documents, calendar files, catalogs, reports, and other file-extension-based assets. | | `other` | Utility, legal, transactional, or unmatched pages that should not be forced into another page type. Includes privacy, terms, login, sign up, account, cart, checkout, and internal search pages. Also used as the fallback when no mapping confidently classifies the page. | ## Fallback behavior Profound uses a closed taxonomy: every page used in Agent Analytics benchmarking resolves to a page type. If no active host-specific mapping matches a URL, Profound applies generic fallback rules for common patterns such as `/blog/`, `/docs/`, `/pricing`, file extensions, infrastructure files, login pages, and legal pages. If neither host-specific mappings nor generic fallback rules confidently classify the page, the page resolves to `other`. This prevents ambiguous URLs from being over-classified while preserving them for reporting. ## How page types are used in benchmarking Benchmarking uses page types to compare pages against the appropriate peer distribution. For example, a product page should be compared against product pages, and a docs page should be compared against docs/support pages. Some benchmarking views show all page types together, while page-specific views may filter to a selected page type. The `other` page type remains available in metrics, but it should be interpreted as a utility or fallback bucket rather than a strategic content category. ## Limitations Page type classification is based on URL patterns and available page evidence. Some sites use ambiguous URL structures, reuse paths for multiple content types, or change templates over time. In those cases, classification may require review and mapping updates. # Getting Started Source: https://docs.tryprofound.com/agent-analytics/self-serve-onboarding Set up Agent Analytics in just a few clicks ## Overview Agent Analytics is Profound's powerful tracking solution that provides comprehensive insights into user interactions on your website. In this guide, you'll learn how to quickly add and configure a domain for tracking, allowing you to monitor traffic patterns, user behavior, and performance metrics. Setting up Agent Analytics takes just a few minutes and requires minimal technical expertise. Once configured, you'll gain access to an intuitive dashboard that displays real-time data, helping you make informed decisions to optimize your website's performance and enhance the user experience. ## Prerequisites * A Profound account * Access to your domain's hosting provider (e.g. Cloudflare, Vercel, etc.) ## Onboarding Navigate to the Agent Analytics tab on the left sidebar. My Website Tab If you are configuring your first domain, you can skip this step. Click on your existing domain near the top and select `+ Add a new website` from the dropdown menu or use the button in the top right. Add a New Website You will then see the following window. Use the input field to enter the apex domain you want to track. Enter Domain Profound will attempt to automatically detect your domain's hosting provider. Ensure the correct provider is selected before continuing. To continue, select the appropriate provider and follow the setup guide. Connect your Profound account to your Cloudflare account with a lightweight worker Connect your Profound account to your Cloudflare account with Logpush } href="/agent-analytics/vercel_native" > Integrate your Vercel account with Agent Analytics in just a few clicks Use Amazon Data Firehose to deliver real-time logs to Profound } href="/agent-analytics/fastly" > Connect your Fastly traffic to Profound with a Custom HTTPS Endpoint } href="/agent-analytics/netlify" > Connect your Netlify traffic to Profound with Log Drains } href="/agent-analytics/akamai" > Connect your Akamai traffic to Profound with DataStream2 } href="/agent-analytics/gcp_log_sink" > Connect your Google Cloud CDN traffic to Profound with a log sink } href="/agent-analytics/wordpress/introduction" > Use the WordPress plugin to send request logs to Profound Build your own custom integration with standardized log drain formats ### Need Help? Our team is here to support you every step of the way. For personalized assistance with your integration, reach out through your dedicated Slack channel. Frequently Asked Questions about Agent Analytics and Google Analytics Monitor our service health and stay updated on system performance in real-time Work with our team to design and build integrations tailored to your unique infrastructure needs ## Troubleshooting * Ensure you have configured the proper domain you want to track * Ensure you selected your hosting provider * Verify you have completed all the steps in the relevant documentation * Be aware it make take several minutes to see data * Reach out to [support](mailto:support@tryprofound.com) # Shopify Source: https://docs.tryprofound.com/agent-analytics/shopify This documentation details 3 potential Shopify log drain integrations for Profound Agent Analytics platform. ## Overview Please carefully read the documentation to determine which integration is best for your Shopify site. Not sure which to use? Please contact [support](mailto:support@tryprofound.com) for assistance. This integration is designed to capture logs from a Shopify site. Shopify is unlike other integrations in that there is no out-of-the-box solution for log exports. We detail three potential integrations for Shopify: 1. Shopify via Nostra (Recommended) 2. Shopify Log Drain via Hydrogen (Requires Hydrogen) 3. Shopify Trace Exporter (Requires Hydrogen) ## Prerequisites * A Shopify site * Access and management permissions to the Shopify site * If connecting via **Nostra**, you must be a customer of both Profound and Nostra. * If connecting via **Hydrogen**, you must have a Shopify Plus account. ## Configuration Shopify setup Learn how to integrate your Shopify site with Profound Partner Nostra Integrating your Shopify site via Nostra is the recommended method with optimal analytics data. This method requires you to use Nostra's platform to track your Shopify site. Ensure you are a Nostra customer hosting your Shopify site on their platform. Contact your Nostra account representative to request a log drain integration. Once the integration is complete, you will be able to see your Shopify site in the Profound dashboard. That's it! Your Shopify site is now integrated with Profound via Nostra. Learn how to integrate your Shopify site with Profound via Hydrogen. This method requires your site uses Hydrogen and you have a Shopify Plus account. Ensure your Shopify site uses Hydrogen and you have a Shopify Plus account. Navigate to the Hydrogen Log Drain setup [documentation](https://shopify.dev/docs/storefronts/headless/hydrogen/logging#connecting-a-log-drain) and follow the instructions to configure the log drain. Select an an `HTTP` log drain for generic integrations. Enter the following URL: `https://artemis.api.tryprofound.com/v1/logs/shopify_hydrogen` Provider a header value `Bearer` with your Profound Log Ingestion Token. Your token should start with `bot_PROFOUND_LOG_INGESTION_TOKEN`. Click **Connect** to connect the log drain. Hydrogen Log Drain Configuration That's it! Your Shopify site is now integrated with Profound via Hydrogen. Learn how to integrate your Shopify site with Profound via the Trace Exporter. This method provides sampled tracing data, not a full log export. This method requires your site uses Hydrogen and you have a Shopify Plus account. Ensure your Shopify site uses Hydrogen and you have a Shopify Plus account. Navigate to the Hydrogen Trace Export setup [documentation](https://shopify.dev/docs/storefronts/headless/hydrogen/trace-exports#configure-trace-exports) and follow the instructions to configure trace exports. Select an `HTTP` trace export for generic integrations. Enter the following URL: `https://artemis.api.tryprofound.com/v1/logs/shopify_hydrogen` Provide a header value `x-api-key` with your Profound Log Ingestion Token. Your token should start with `bot_PROFOUND_LOG_INGESTION_TOKEN`. Click **Save** to save the trace export configuration. That's it! Your Shopify site is now integrated with Profound via the Trace Exporter. ## Limitations of Hydrogen-Based Integrations **Important**: Shopify's Hydrogen platform does not expose client IP addresses in log drains or trace exports. This is a platform limitation, not a Profound limitation. ### The Issue Profound's Agent Analytics uses a multi-step verification process to accurately classify traffic — distinguishing real users from bots, crawlers, and automated agents. A key input to this process is the client IP address. Shopify's Hydrogen runtime, hosted on Oxygen, strips client IP information from logs and traces before they reach external consumers. Because Profound never receives IP data through these integrations, the multi-step verification process cannot be performed. This is an unavoidable platform constraint imposed by Shopify. There is no configuration, workaround, or API option to include IP addresses in Hydrogen log drains or trace exports. ### Impact on Analytics Without client IP addresses, Profound cannot: * Perform IP-based reputation checks against known bot networks * Correlate requests across sessions for behavioral analysis * Apply geographic and network-level classification signals * Execute the full multi-step verification pipeline Analytics data from Hydrogen-based integrations will still be collected and displayed, but classification accuracy is reduced compared to integrations that provide complete request metadata. ### Recommendation We recommend integrating your Shopify site via **Nostra** (option 1 above). Nostra's platform uses a Cloudflare integration under the hood, which provides full request metadata — including client IP addresses. This means the Nostra integration does not suffer from any of the limitations described above, and Profound can run the complete multi-step verification process. Using the Nostra integration requires you to be a customer of both **Profound** and **Nostra**. This limitation is not unique to Agent Analytics. Any third-party service consuming Hydrogen log drains or trace exports faces the same constraint — Shopify simply does not include client IP data in these exports. The recommended path for complete traffic visibility on Shopify is through a CDN-native integration like Nostra. # Vercel Integration (Connectable Account) Source: https://docs.tryprofound.com/agent-analytics/vercel_native This guide explains how to set up a Vercel Log Drain via the Profound Agent Analytics Integration ## Overview Vercel Log Drains provide a way to forward your Vercel application logs to [Profound Agent Analytics](https://vercel.com/cooper-square-technologies/~/integrations/profound) platform. By integrating with Profound's Agent Analytics platform, you gain access to comprehensive monitoring features including automated agent detection and real-time performance analysis. ## Prerequisites Before beginning the integration process, ensure you have: * A Vercel account with administrative access * A Profound account with an active subscription * Administrative access to your Vercel project settings ## Configuration Vercel Detected Log into your Vercel Dashboard and navigate to your projects settings at **Settings** > **Log Drains**. Settings In the top right, press the **Add Log Drain** button. This will open a list of integrations. Search for "Profound Agent Analytics" and select it. Press **Continue**. Profound AA Integration You will now be prompted to configure the log drain. Select **Specific Projects** and add the projects you would like to track via Profound's Agent Analytics service. Press **Connect Account** when ready. All projects you give access to will be configured for Profound's Agent Analytics. It is critical you select only the projects you wish to track in Profound. **Do not share access to projects you do not intend to track via Profound.** Select Projects You will be redirected to Profound to finish the configuration. If you are logged out, you will see a login prompt. Logged Out Simply enter your email, complete any OAuth or SSO steps, and you will be returned to the page below. Agent Analytics requires access to the Profound platform and thus a profound account. If you do not have an account, press **I'm new to Profound**. Logged Out This will open the pricing page in a new tab. Press **Start** to proceed. Pricing Because a Profound account is required, you must complete this process **before** completing the integration. Onboard Double check the projects and domains in the list. These are the domains that will be tracked via Agent Analytics. Likewise, ensure your Profound Organization is selected. If you are a member of many organizations, double check the proper one is selected. Press **Continue** when ready. Verify Projects and Domains After pressing **Continue**, Vercel will verify the integration. Vercel Verify You will be returned to the integration homepage. Integration Page That's it! Vercel will now send real-time logs to Profound. Data should begin appearing in your dashboard within a few minutes. ## Remove Integration There are a few ways to disable the integration. 1. Option 1 Select the project you configured. Navigate to **Settings** > **Log Drains**. Log Drains Press **Manage**. 2. Option 2 Navigate to **Project** > **Integrations** in the top toolbar. Manage Integrations Press **Manage**. Either option will redirect you to the integration page. Integration Page Press **Settings** in the navigation tabs at the top. To remove the integration, scroll to the bottom of the **Settings** page and press **Uninstall Integration** and complete the steps required by Vercel. Uninstall You have now removed the integration. Logs will no longer be sent to Profound Agent Analytics. ## Additional Resources * [Integration Documentation](https://vercel.com/cooper-square-technologies/~/integrations/profound-test) * [Vercel Log Drain Documentation](https://vercel.com/docs/observability/log-drains) * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for integration assistance # WordPress Plugin Installation Source: https://docs.tryprofound.com/agent-analytics/wordpress/installation This guide explains how to install and configure the Agent Analytics plugin for WordPress to send HTTP request logs to the Profound platform The wordpress plugin can have limited functionality by nature of the WordPress platform. Please read the [limitations](/agent-analytics/wordpress/limitations) page to understand more. ## Overview The Agent Analytics [WordPress plugin](https://wordpress.org/plugins/profound-agent-analytics/) enables automatic log collection from your WordPress site, forwarding visitor traffic data to the Profound Agent Analytics platform. The plugin operates transparently in the background, capturing only public traffic while excluding all administrative and system activity. ## Prerequisites Before beginning the installation, ensure you have: * A WordPress site running version 6.0 or higher * Administrative access to your WordPress dashboard * A Profound account with a Log Ingestion Token * PHP 7.4 or higher on your hosting environment ## Installation Wordpress Detected Log into your WordPress admin dashboard and navigate to **Plugins** in the left sidebar. Click **Add New** at the top of the Plugins page. Search for "Profound Agent Analytics" or visit the [WordPress plugin page](https://wordpress.org/plugins/profound-agent-analytics/) to download the plugin. WordPress Plugins Navigation Click **Install Now** to install the plugin. After installation completes, click **Activate** to activate the plugin. Alternatively, navigate to **Plugins** → **Installed Plugins**, find "Agent Analytics" in the list, and click **Activate**. Activate Plugin Once activated, navigate to **Settings** → **Agent Analytics** in your WordPress admin menu. In the General settings tab: 1. Check the **Enable Logging** checkbox to activate log collection 2. Enter your Profound Log Ingestion Token (starts with `bot_`) in the **Log Ingestion Token** field 3. Leave the **API Endpoint** at its default value unless instructed otherwise 4. Optionally adjust the **Batch Size** and **Send Interval** settings. API Configuration Your Log Ingestion Token is sensitive information. For production sites, consider using environment variables instead of database storage. See the [security documentation](/agent-analytics/wordpress/security) for details. Navigate to the **Privacy** tab to adjust filtering options: * **Query Parameter Redaction**: Add any additional sensitive parameters to redact (default includes passwords, tokens, cards) * **Path Filtering**: Add custom paths to exclude from logging * **Skip Options**: Toggle exclusion of admin, cron, AJAX, or REST API requests Privacy Settings The plugin automatically excludes all WordPress admin activity and system paths. These settings allow additional customization beyond the defaults. Click **Save Changes** at the bottom of the settings page. Navigate to the **Status** tab to verify your configuration: 1. Check that the **Queue Status** shows as "Active" 2. Click **Send Test Event** to verify connectivity 3. Confirm you see "Test event sent successfully" Status Verification If the test fails, double-check your Log Ingestion Token and ensure your server can make outbound HTTPS requests to `artemis.api.tryprofound.com`. After configuration: 1. Visit a few pages on your public website (not logged into WordPress admin) 2. Return to **Settings** → **Agent Analytics** → **Status** 3. Check that the **Next automatic send** shows a time within the next minute 4. Verify the **Queue size** increases as you browse, then decreases after automatic sending That's it! You have successfully configured Agent Analytics for your WordPress site. Data should begin appearing in your Profound dashboard within minutes. ## Post-Installation Verification To confirm everything is working correctly: 1. **Check the Status Tab**: The queue should periodically drain (every 60 seconds by default) 2. **Monitor Your Dashboard**: Log into your Profound account and verify data is appearing 3. **Review Processing Schedule**: Ensure "Next automatic send" updates regularly ## Troubleshooting ### Logs Not Sending * Verify your Log Ingestion Token is correct and starts with `bot_` * Check that **Enable Logging** is checked in General settings * Ensure WordPress Cron is running (visit your site frontend to trigger it) * Look for any error messages in the Status tab ### Test Event Fails * Confirm your server allows outbound HTTPS connections * Check if a firewall is blocking connections to `artemis.api.tryprofound.com` * Verify your Log Ingestion Token has no extra spaces or characters ### Queue Growing But Not Sending * The automatic sending requires site visits to trigger WordPress Cron * Consider setting up a real cron job for consistent processing * Click **Re-schedule Automatic Sending** if the schedule appears missing ### Wrong IP Addresses * The plugin automatically detects proxy headers from CDNs and managed hosts * If IPs appear incorrect, your host may use non-standard headers * Contact support for assistance with custom IP detection ## Additional Resources * [Security & Privacy Configuration](/agent-analytics/wordpress/security) - Detailed privacy settings and data handling * [Plugin Overview](/agent-analytics/wordpress/introduction) - Complete feature documentation * Contact [support@tryprofound.com](mailto:support@tryprofound.com) for integration assistance # Introduction Source: https://docs.tryprofound.com/agent-analytics/wordpress/introduction Send request logs from your WordPress site to Agent Analytics for advanced traffic analysis The wordpress plugin can have limited functionality by nature of the WordPress platform. Please read the [limitations](/agent-analytics/wordpress/limitations) page to understand more. ## Overview The Agent Analytics WordPress plugin provides a seamless integration for WordPress sites to send HTTP request logs to the Profound Analytics platform. The plugin is designed with performance and privacy in mind, capturing only public visitor traffic while excluding all administrative and system activity. Asynchronous queue system ensures page delivery is never blocked. Adds less than 1ms to request processing. Automatically excludes WordPress admin, REST API, and system paths. Only tracks real visitor traffic. Redacts sensitive query parameters and excludes login pages. No cookies or client-side tracking. Supports environment variables for Log Ingestion Tokens and includes circuit breaker protection for reliability. ## How It Works The plugin operates using a lightweight, non-blocking architecture: 1. **Capture** - Essential HTTP metadata is captured during request processing 2. **Queue** - Data is immediately stored in a local database queue with minimal overhead 3. **Batch** - Background process sends logs in efficient batches (up to 1,000 per request) 4. **Retry** - Failed sends are automatically retried with exponential backoff 5. **Protect** - Circuit breaker prevents cascade failures if the API is unavailable ## System Requirements * WordPress 6.0 or higher * PHP 7.4 or higher * MySQL 5.7+ or MariaDB 10.2+ * Outbound HTTPS connectivity * WordPress Cron enabled (or system cron configured) * Sodium PHP extension (recommended for Log Ingestion Token encryption) ## Hosting Compatibility The plugin is tested and optimized for: * **Managed WordPress Hosts**: WP Engine, Kinsta, Flywheel * **Cloud Platforms**: AWS, Google Cloud, Azure * **Traditional Hosts**: SiteGround, Bluehost, GoDaddy * **Local Development**: LocalWP, MAMP, XAMPP The plugin automatically detects and handles reverse proxy headers used by managed WordPress hosts and CDNs to ensure accurate IP detection. ## Next Steps Configure Log Ingestion Tokens, privacy settings, and filtering options Understand the limitations of the WordPress plugin when used with a CDN for caching Step-by-step instructions for installing and activating the plugin *** # CDN Caching Limitations Source: https://docs.tryprofound.com/agent-analytics/wordpress/limitations Understand the limitations of the WordPress plugin when used with a CDN for caching **Important**: If your WordPress host uses a CDN for caching (Cloudflare, Fastly, etc.), the plugin will **only capture cache misses** — not cache hits. This is a platform limitation, not a plugin limitation. ### The Issue Many managed WordPress hosts (WP Engine, Kinsta, Flywheel, etc.) place a CDN in front of your WordPress site for performance. When a visitor requests a page: 1. **Cache Miss** → Request passes through to WordPress → Plugin captures the request → Logged successfully 2. **Cache Hit** → CDN serves cached content directly → **Request never reaches WordPress** → Not logged by plugin This means you'll only see a portion of your actual traffic — specifically, the traffic that wasn't served from cache. #### Example In this example, the site is hosted on WordPress Engine with WPE application cache layer enabled. In this example, we are fetching the page for the first time, so it is a cache miss. ```shell theme={null} curl -I "https://my-wp-site.com/some-page" HTTP/2 200 OK ... x-cache: MISS # Cache missed, request reaches WordPress and is logged by the plugin ... ``` On the second request, the page is live in the cache, so it is served from the cache layer and never reaches WordPress. ```shell theme={null} curl -I "https://my-wp-site.com/some-page" HTTP/2 200 OK ... x-cache: HIT: 1 # Cache hit, request never reaches WordPress and is not logged by the plugin ... ``` ### Why This Happens The plugin operates at the WordPress application layer. It can only log requests that actually reach your WordPress server. When a CDN serves cached content, the request terminates at the CDN's edge servers and never makes it to your WordPress installation. **The complete solution** is to use CDN-native log drains, which Agent Analytics already fully supports. CDN logs capture *everything* — cache hits, cache misses, bot traffic, DDoS attempts, the complete picture. ### The Platform Problem Unfortunately, most managed WordPress hosts like WP Engine and Kinsta do not provide customers access to configure their CDN log drains. Profound has contacted these providers to request access for customers, they declined at this time. We are pursuing partnership opportunities with these providers to enable this functionality. ### Impact on Analytics * Initial page visits (cache miss) * Dynamic/personalized pages that bypass cache * POST requests and form submissions * Admin-generated traffic to public pages * Pages with query parameters (often uncached) * Requests during cache purge/rebuild periods * Subsequent visits to the same page (cache hits) * Popular pages with high cache hit rates (90%+ typical) * Static assets served from CDN (CSS, JS, images) * Bot traffic filtered or served at CDN edge * Attacks/DDoS blocked at CDN layer * **Well-optimized sites**: 85-95% cache hit rate * **Average sites**: 60-80% cache hit rate * **Dynamic/personalized sites**: 20-40% cache hit rate ### Workarounds If you need complete traffic visibility, consider: 1. **Switch to a host that provides CDN control** - Many hosts (AWS, Google Cloud, etc.) allow you to configure your own CDN 2. **Request enterprise CDN access** - Some managed hosts provide log access to enterprise customers 3. **Accept partial data** - For bot detection and abuse monitoring, cache misses are often sufficient since bots typically don't benefit from cache 4. **Use Agent Analytics directly with a supported CDN** - If you have the ability to configure your own Cloudflare, Fastly, or other supported CDN, use our native integrations instead This limitation is not unique to Agent Analytics. Any WordPress plugin attempting to log HTTP requests faces the same constraint — they can only see what reaches WordPress. The industry standard solution is CDN-native log collection, which we fully support for customers with CDN access. # Security & Privacy Configuration Source: https://docs.tryprofound.com/agent-analytics/wordpress/security Configure data collection, filtering, and privacy settings for the WordPress plugin ## Privacy & Compliance The plugin is designed to work with analytics while respecting user privacy: * **No cookies**: Pure server-side implementation * **No JavaScript**: No client-side tracking code * **Data minimization**: Only HTTP metadata, no content * **Automatic redaction**: Sensitive parameters removed * **Public only**: Admin and user areas excluded Full IP addresses are collected as they are essential for accurate bot detection and traffic analysis. If your jurisdiction requires IP anonymization, additional configuration may be needed. ## Data Collection The plugin collects standard HTTP request metadata from public pages: ### Required Fields The following fields are **required** for every log entry sent to Agent Analytics: | Field | Format | Max Length | Description | | ------------- | -------- | ---------- | -------------------------------------- | | `timestamp` | ISO 8601 | - | UTC timestamp of the request | | `method` | String | 10 chars | HTTP method (GET, POST, etc.) | | `host` | String | 255 chars | Request hostname | | `path` | String | 2048 chars | URL path (without query string) | | `status_code` | Integer | 100-599 | HTTP response status code | | `ip` | String | 45 chars | Client IP address (supports IPv4/IPv6) | | `user_agent` | String | 1024 chars | User agent string | All required fields must be present for successful log ingestion. Missing fields will result in validation errors on the API side. ### Optional Fields These fields are included when available but not required: | Field | Format | Max Length | Description | | -------------- | ------- | ---------- | ----------------------------------------- | | `query_params` | Object | 100 keys | Query string parameters (after redaction) | | `referer` | String | 2048 chars | HTTP referer header | | `bytes_sent` | Integer | - | Response size in bytes | | `duration_ms` | Integer | - | Request processing time | The plugin **does not** collect: page content, form data, WordPress user information, admin activity, database queries, or any server-side application data. ## Automatic Path Filtering ### Always Excluded Paths The following paths are **automatically excluded** from logging and cannot be overridden: ``` wp-admin # All admin pages and subdirectories wp-login.php # Login page wp-cron.php # Cron jobs admin-ajax.php # Admin AJAX calls xmlrpc.php # XML-RPC interface wp-json/wp/* # WordPress core REST API ``` These exclusions use substring matching, so they work regardless of WordPress installation directory (e.g., `/blog/wp-admin/` is also excluded). ### Request Type Exclusions The plugin automatically skips: * **Admin-referred requests**: Any request with a referer containing `wp-admin` * **REST API calls**: Requests with `rest_route=/wp/*` query parameter * **CLI operations**: WP-CLI commands * **Edit context**: REST requests with `context=edit` parameter ### Path Filtering Priority Paths are evaluated in this order: 1. **Built-in exclusions** (wp-admin, wp-login.php, etc.) 2. **Custom deny paths** (user-configured patterns) 3. **Referer check** (requests from wp-admin) 4. **REST API check** (WordPress core API) First match wins - if any check matches, the request is excluded. ## Query Parameter Redaction ### Default Redacted Parameters The following query parameter keys are automatically redacted (replaced with `[REDACTED]`): ``` password # Password fields pass # Alternative password fields token # Authentication tokens secret # Secret keys card # Credit card fields cc # Credit card abbreviation ssn # Social security numbers ``` Redaction uses substring matching. Any parameter containing these strings will be redacted (e.g., `user_password`, `api_token`, `card_number`). ### Redaction Example Original query string: ``` ?username=john&password=secret123&product=widget&api_token=abc123 ``` After redaction: ```json theme={null} { "username": "john", "password": "[REDACTED]", "product": "widget", "api_token": "[REDACTED]" } ``` ## Log Ingestion Token Security ### Storage Methods The plugin supports three methods for Log Ingestion Token configuration, in order of security preference: Add to your server environment: ```bash theme={null} WP_ENV_AGENT_ANALYTICS_API_KEY=bot_PROFOUND_LOG_INGESTION_TOKEN ``` Or in `wp-config.php`: ```php theme={null} define('WP_ENV_AGENT_ANALYTICS_API_KEY', 'bot_PROFOUND_LOG_INGESTION_TOKEN'); ``` Add to `wp-config.php`: ```php theme={null} define('AGENT_ANALYTICS_API_KEY', 'bot_PROFOUND_LOG_INGESTION_TOKEN'); ``` Enter via WordPress admin. The key is: * Encrypted using Sodium library (if available) * Falls back to WordPress salts for encryption * Stored in `wp_options` table * Masked in UI (only last 4 characters shown) For production environments, use environment variables or constants. Database storage should only be used for testing or when file access is restricted. ## IP Address Handling ### Detection Order The plugin attempts to detect the real client IP in this order: 1. `HTTP_X_FORWARDED_FOR` - First IP in the list (for proxies/load balancers) 2. `HTTP_CF_CONNECTING_IP` - Cloudflare's real IP header 3. `HTTP_X_REAL_IP` - Alternative proxy header 4. `HTTP_TRUE_CLIENT_IP` - Cloudflare Enterprise header 5. `REMOTE_ADDR` - Direct connection (fallback) The plugin automatically handles reverse proxies used by managed WordPress hosts (WP Engine, Kinsta, etc.) and CDNs (Cloudflare, Fastly). ### IP Privacy Considerations * **No anonymization**: Full IP addresses are captured for accurate bot detection * **Required for service**: IP data is essential for geographic analysis and reverse DNS lookups * **GDPR compliance**: Consider your legal requirements for IP data collection ## Configuration Options ### General Settings | Setting | Default | Description | | ------------------ | ------------------------------------------------------- | ------------------------------------------- | | **Enable Logging** | Off | Master switch for log collection | | **API Endpoint** | `https://artemis.api.tryprofound.com/v1/logs/wordpress` | Destination for logs | | **Batch Size** | 500 | Number of logs per send (50-1000) | | **Send Interval** | 60 seconds | How often to process queue (30-600 seconds) | ### Privacy Settings | Setting | Default | Options | Description | | --------------------- | ---------------------------------------- | -------------------- | --------------------------------------------- | | **Skip Admin** | On | On/Off | Exclude `/wp-admin/` requests | | **Skip Cron** | On | On/Off | Exclude cron job requests | | **Skip AJAX** | On | On/Off | Exclude admin AJAX calls | | **Skip REST** | Off | On/Off | Exclude ALL REST API requests (not just core) | | **Redact Query Keys** | `password,pass,token,secret,card,cc,ssn` | Comma-separated list | Parameters to redact | ### Advanced Settings | Setting | Default | Description | | ----------------------------- | ---------- | ---------------------------------------------------- | | **Custom Deny Paths** | Empty | Newline-separated path patterns (supports wildcards) | | **Max Queue Size** | 100,000 | Maximum logs to store before dropping old entries | | **Retention Days** | 7 | Days to keep unsent logs | | **Circuit Breaker Threshold** | 3 | Consecutive failures before pausing | | **Circuit Breaker Cooldown** | 30 minutes | Pause duration after threshold reached | ## Custom Path Filtering ### Wildcard Pattern Examples Add custom path exclusions using these patterns: ``` /private/* # Exclude all paths starting with /private/ */temp/* # Exclude paths containing /temp/ *.pdf # Exclude all PDF files /api/v1/internal/* # Exclude internal API endpoints staging.* # Exclude staging subdomains ``` ## Data Retention ### Plugin-Side Retention * **Queue entries**: Deleted after successful send * **Failed entries**: Retried up to 6 times with exponential backoff * **Old entries**: Auto-cleaned after retention period (default 7 days) * **Maximum queue**: Oldest entries dropped when limit reached (100,000) ### API-Side Processing * **Successful logs**: Processed immediately by Agent Analytics * **Validation errors**: Logged but entry still deleted from queue * **Authentication failures**: Queue paused, circuit breaker activated ## Performance Characteristics * **Processing overhead**: \< 1ms per request * **Memory usage**: Minimal (single array per request) * **Database operations**: One INSERT per request * **No output buffering**: Does not delay response delivery * **Send interval**: Every 60 seconds (configurable) * **Batch size**: Up to 500 logs per send (configurable) * **Timeout**: 5 second maximum per batch * **Queue limit**: 100,000 entries maximum * **Automatic retry**: Exponential backoff on failures * **Circuit breaker**: Pauses after 3 consecutive failures * **Queue persistence**: Logs preserved during outages * **Lock mechanism**: Prevents concurrent processing ## Security Best Practices * Store Log Ingestion Tokens in environment variables rather than the database for production sites. * Ensure all sensitive areas of your site are excluded from logging. * Check the Status tab regularly to ensure logs are sending properly. * Use the "Send Test Event" feature to verify connectivity without affecting real data. * Ensure your WordPress site uses HTTPS to protect data in transit to your server. If you discover any security issues or need assistance with configuration, please contact [support@tryprofound.com](mailto:support@tryprofound.com) immediately. # Get an agent Source: https://docs.tryprofound.com/api-reference/agents/get-an-agent https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/agents/{agent_id} Retrieve an agent and its schema details. Agents can have both a live published version and a draft version with newer unpublished changes. Use the `version` parameter to choose which state to return. # Get an agent run Source: https://docs.tryprofound.com/api-reference/agents/get-an-agent-run https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/agents/{agent_id}/runs/{run_id} Retrieve the current status and result details for an agent run. # List agents Source: https://docs.tryprofound.com/api-reference/agents/list-agents https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/agents List agents available to your organization. Agent status reflects whether an agent has ever been published. `published` agents have a live published version. `draft` agents have not been published yet. # Run an agent Source: https://docs.tryprofound.com/api-reference/agents/run-an-agent https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/agents/{agent_id}/runs Start a new run for an agent. Runs always execute the agent's live published version, so the agent must be published first with `POST /v1/agents/{agent_id}/publish`. Unpublished drafts cannot be run. # Optimization Analysis Source: https://docs.tryprofound.com/api-reference/content/optimization-analysis https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/content/{asset_id}/optimization/{content_id} # Optimization List Source: https://docs.tryprofound.com/api-reference/content/optimization-list https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/content/{asset_id}/optimization # Add Document Source: https://docs.tryprofound.com/api-reference/knowledge-bases/add-document https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/knowledge-bases/{knowledge_base_id}/documents Add a document to a knowledge base using JSON text or multipart file upload. # Add Folder Source: https://docs.tryprofound.com/api-reference/knowledge-bases/add-folder https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/knowledge-bases/{knowledge_base_id}/folders Create an empty folder at the requested knowledge base path. # Delete Document Source: https://docs.tryprofound.com/api-reference/knowledge-bases/delete-document https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml delete /v1/knowledge-bases/{knowledge_base_id}/documents Delete an existing document from a knowledge base. # Delete Folder Source: https://docs.tryprofound.com/api-reference/knowledge-bases/delete-folder https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml delete /v1/knowledge-bases/{knowledge_base_id}/folders Delete a folder. With recursive=false, non-empty folders return 409 and no contents are deleted. # List Knowledge Bases Source: https://docs.tryprofound.com/api-reference/knowledge-bases/list-knowledge-bases https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/knowledge-bases List knowledge bases accessible to the API key. # Search Knowledge Base Source: https://docs.tryprofound.com/api-reference/knowledge-bases/search-knowledge-base https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/knowledge-bases/{knowledge_base_id}/search Search a knowledge base and return matching snippets or pages. # Update Document Source: https://docs.tryprofound.com/api-reference/knowledge-bases/update-document https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml put /v1/knowledge-bases/{knowledge_base_id}/documents Overwrite a knowledge base document using JSON text or multipart file upload. # Get Account Insights Source: https://docs.tryprofound.com/api-reference/openai-ads/get-account-insights https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/ads/openai-ads/ad-account/insights Get ad account insights for the organization's OpenAI Ads partner brand. `aggregation_level=campaign` returns one row per campaign (with `campaign_id` / `campaign_name` and all metrics), so every campaign's insights come back in a single call; `time_granularity=daily` gives per-day rows (e.g. daily spend). # Create prompts Source: https://docs.tryprofound.com/api-reference/organization/create-prompts https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/org/categories/{category_id}/prompts Create one or more prompts in a category. Topics and tags are auto-created if referenced by name and not yet existing. Use dry_run to preview without persisting. # Get Assets Source: https://docs.tryprofound.com/api-reference/organization/get-assets https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/assets Get the organization assets, one row per (asset, organization) pair. An asset's category can belong to multiple organizations; one asset row is emitted per owning org so no association is silently dropped. # Get Categories Source: https://docs.tryprofound.com/api-reference/organization/get-categories https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories Get the organization categories, one row per (category, organization) pair. # Get Category Assets Source: https://docs.tryprofound.com/api-reference/organization/get-category-assets https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/assets # Get Category Citation Categories Source: https://docs.tryprofound.com/api-reference/organization/get-category-citation-categories https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/citation-categories Get the citation categories for a category: the built-in buckets plus any custom categories. # Get Category Citation Tags Source: https://docs.tryprofound.com/api-reference/organization/get-category-citation-tags https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/citation-tags Get the custom citation tags defined for a category. # Get Category Personas Source: https://docs.tryprofound.com/api-reference/organization/get-category-personas https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/personas # Get Category Regions Source: https://docs.tryprofound.com/api-reference/organization/get-category-regions https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/regions Get the regions for a specific category. # Get Category Tags Source: https://docs.tryprofound.com/api-reference/organization/get-category-tags https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/tags Get the tags for a specific category. # Get Category Topics Source: https://docs.tryprofound.com/api-reference/organization/get-category-topics https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/topics Get the topics for a specific category. # Get Domains Source: https://docs.tryprofound.com/api-reference/organization/get-domains https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/domains Get the organization domains. # Get Models Source: https://docs.tryprofound.com/api-reference/organization/get-models https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/models Get the organization models. # Get Personas Source: https://docs.tryprofound.com/api-reference/organization/get-personas https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/personas Get the organization personas, one row per (persona, organization) pair. Same (item, org) fan-out as ``get_assets``: a persona's category can be owned by multiple orgs, and each owning org gets its own row so no association is silently dropped. # Get Regions Source: https://docs.tryprofound.com/api-reference/organization/get-regions https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/regions Get the organization regions. # List prompts Source: https://docs.tryprofound.com/api-reference/organization/list-prompts https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml get /v1/org/categories/{category_id}/prompts Retrieve prompts in a category with optional filtering by type, topic, tag, region, platform, or persona. Supports cursor-based pagination. # Update prompt status Source: https://docs.tryprofound.com/api-reference/organization/update-prompt-status https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml patch /v1/org/categories/{category_id}/prompts/status Bulk-update the status of one or more prompts. Prompts already in the target status are skipped. Use dry_run to preview without persisting. Status options: - 'active': Prompts will run daily. - 'disabled': Prompts will not run moving forward, but historical data is preserved. - 'deleted': Prompts are deleted along with historical data # Update prompts Source: https://docs.tryprofound.com/api-reference/organization/update-prompts https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml patch /v1/org/categories/{category_id}/prompts Update one or more existing prompts. Only provided fields are changed. Dimension fields (regions, platforms, personas, tags) replace the full set when provided. Use dry_run to preview without persisting. # Get Answers Source: https://docs.tryprofound.com/api-reference/prompts/get-answers https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/prompts/answers # Get Bots Report V1 Source: https://docs.tryprofound.com/api-reference/reports/get-bots-report-v1 https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/reports/bots Get bot traffic report from the daily aggregated materialized view. This endpoint queries pre-aggregated daily bot data, making it efficient for large date ranges and high-traffic sites. Metrics: - count: unique bot visits - citations: unique citation events - indexing: unique indexing events - training: unique training events - last_visit: most recent visit timestamp # Get Bots Report V2 Source: https://docs.tryprofound.com/api-reference/reports/get-bots-report-v2 https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v2/reports/bots Get bot traffic report from the hourly aggregated materialized view (UTC-based). Supports date_interval="hour", calendar intervals through "year", "quarter", and "relative_week". Metrics: - count: unique bot visits - citations: unique citation events (ai_assistant bot type) - indexing: unique indexing events (index bot type) - training: unique training events (ai_training bot type) - last_visit: most recent visit timestamp Dimensions: - date, path, bot_name, bot_provider, bot_type # Get Referrals Report V1 Source: https://docs.tryprofound.com/api-reference/reports/get-referrals-report-v1 https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/reports/referrals Get referral traffic report from the daily aggregated materialized view. This endpoint queries pre-aggregated daily referral data, making it efficient for large date ranges and high-traffic sites. # Get Referrals Report V2 Source: https://docs.tryprofound.com/api-reference/reports/get-referrals-report-v2 https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v2/reports/referrals Get referral traffic report from the hourly aggregated materialized view (UTC-based). Supports date_interval="hour", calendar intervals through "year", "quarter", and "relative_week". # Query Citations Source: https://docs.tryprofound.com/api-reference/reports/query-citations https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/reports/citations Get citations for a given category. The ``mentioned`` filter supports ``is true`` and ``is false``. It uses the latest page analysis available at or before ``end_date``; pages without an analysis by then are excluded from both values. ``citation_share`` keeps all otherwise eligible citations in its denominator when this filter is used. # Query Fanouts Source: https://docs.tryprofound.com/api-reference/reports/query-fanouts https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/reports/query-fanouts # Query Sentiment Source: https://docs.tryprofound.com/api-reference/reports/query-sentiment https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/reports/sentiment Get citations for a given category. # Query Visibility Source: https://docs.tryprofound.com/api-reference/reports/query-visibility https://app.stainless.com/api/spec/documented/profound/openapi.documented.yml post /v1/reports/visibility Query visibility report. # Introduction Source: https://docs.tryprofound.com/bots/introduction An overview of Profound bots Profound operates a small number of automated HTTP agents that fetch publicly available data from the web on behalf of our customers. This section documents each bot, what it does, when it runs, and how site operators can identify, allow, or block it. ## Active bots | Name | User-Agent | Trigger | Purpose | | ---------------------------------------------------------- | ------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------- | | [ProfoundBot](/bots/profoundbot) | `ProfoundBot/1.0 (+https://docs.tryprofound.com/bots)` | User-initiated | Fetches a single public web page on demand so you can read its content as Markdown, HTML, or both | | [Profound Sheets Sitemap Importer](/bots/sitemap-importer) | `ProfoundSheetsSitemapImporter/1.0` | User-initiated | Fetches a website's `sitemap.xml` and `robots.txt` so you can import sitemap URLs into a Profound Sheet | ## Bot operating rules Every Profound bot follows the same operating rules: * **Public data only:** Our bots fetch resources that are intended to be publicly readable (e.g. `sitemap.xml`, `robots.txt`, public HTML pages). They never attempt authenticated areas, never submit forms, and never bypass access controls. * **User-Agent identification:** Every bot sends a stable, descriptive User-Agent header with a name that begins with `Profound`. Site operators can match on that prefix to identify all Profound traffic. * **`robots.txt` awareness:** Our bots read `robots.txt` for any host they fetch from. Bots that crawl HTML pages honor `Disallow` directives. * **No persistent crawling without disclosure:** Any bot that runs on a schedule, recursively follows links, or maintains an index has a dedicated page in this section documenting its behaviour in detail. Reach us at [security@tryprofound.com](mailto:security@tryprofound.com) to report abuse, request blocking, or ask questions about the traffic you have observed. We treat security seriously and respond to all reports. # ProfoundBot Source: https://docs.tryprofound.com/bots/profoundbot ProfoundBot is a user-initiated HTTP agent. When you supply a URL through the Profound platform or API, it fetches that public web page and returns its content as Markdown and/or HTML. ## Summary | | | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Operator** | Profound ([tryprofound.com](https://tryprofound.com)) | | **User-Agent** | `ProfoundBot/1.0 (+https://docs.tryprofound.com/bots)` | | **Trigger** | User-initiated | | **Schedule** | None | | **Resources fetched** | The single URL the customer supplies | | **Network origin** | AWS by default; enrolled domains are fetched from a fixed set of dedicated egress IPs (see [Network origin and dedicated IPs](#network-origin-and-dedicated-ips) below) | ## What it does When you supply a public webpage URL to read the content from, ProfoundBot makes a bounded set of HTTP requests to that URL's origin to fetch the page, then returns the content as Markdown, HTML or both. When a page is rendered with a headless browser, that page's own subresources (scripts, stylesheets, images) load as they would in any browser. ProfoundBot doesn't follow links to other pages. ## What it doesn't do * **Crawl.** Each customer action fetches exactly one URL. The bot does not recursively follow links or build an index of your site * **Submit forms**, follow login flows, or access authenticated areas * **Persist** a long-running index of your site. Fetched content is delivered to you and is not republished * **Reach private or internal network targets** ## Request behavior Each user action triggers a small, bounded interaction scoped to the single requested URL. | | | | ------------- | ------------------------------------------------------------------ | | **Redirects** | Followed | | **Caching** | None: each customer invocation issues fresh requests | | **Headers** | `User-Agent: ProfoundBot/1.0 (+https://docs.tryprofound.com/bots)` | ## Network origin and dedicated IPs By default, ProfoundBot egresses from a dynamic IP range. Identify it by its User-Agent. You can enroll a domain (a per-domain setting, off by default) so that ProfoundBot fetches it from a fixed set of dedicated egress IPs. This is useful if you prefer to allowlist Profound by IP rather than by User-Agent. When a domain is enrolled, requests egress from one of these IPs: * `54.71.251.60` * `54.185.59.110` * `100.22.234.65` These dedicated IPs are also available in a [machine-readable JSON format](/bots/profoundbot.json). To enroll a domain for dedicated-IP fetching, go to your Profound account **Settings → Web Scrape** and toggle the **Primary Domain Scraping via Static IPs** setting to **On**. ## `robots.txt` handling ProfoundBot is user-initiated and fetches a single page per user action rather than crawling. Support for honoring `Disallow` directives in `robots.txt` is planned for a future release. To prevent ProfoundBot from accessing your site in the meantime, see [How to block](#how-to-block) section. ## How to identify the bot Use the exact User-Agent header: ``` User-Agent: ProfoundBot/1.0 (+https://docs.tryprofound.com/bots) ``` All Profound bots use User-Agent strings that start with `Profound`, so a prefix match is a good way to identify all Profound traffic, current and future. For enrolled domains, you can also match on the dedicated egress IPs listed in the [Network origin and dedicated IPs](#network-origin-and-dedicated-ips) section. ## How to block Block ProfoundBot using any of the following methods: * A WAF or CDN rule matching User-Agent containing `ProfoundBot` * A WAF or CDN rule matching User-Agent containing the prefix `Profound` * For enrolled domains, a firewall or WAF rule blocking the dedicated egress IPs (`54.71.251.60`, `54.185.59.110`, `100.22.234.65`) ## Reporting abuse If you observe behavior that doesn't match this documentation, such as recursive crawling, request rates inconsistent with user-initiated single-page fetches, or activity from an unrecognized `Profound*` User-Agent, report it to [security@tryprofound.com](mailto:security@tryprofound.com) with example log lines. Profound treats these reports as security issues and responds to all of them. # Profound Sheets Sitemap Importer Source: https://docs.tryprofound.com/bots/sitemap-importer The Profound Sheets Sitemap Importer is a user-initiated HTTP agent that fetches a website's sitemap for its URLs to be imported into a Profound Sheet. This bot only runs when an authenticated Profound user clicks **+ Create new → Sitemap** inside the Sheets section of the Profound platform and supplies a site URL. ## Summary | | | | --------------------- | ---------------------------------------------------------------------------------------------------- | | **User-Agent string** | `ProfoundSheetsSitemapImporter/1.0` | | **Trigger** | User-initiated | | **Schedule** | None | | **Resources fetched** | `sitemap.xml`, sitemap-index variants, `robots.txt`, and any sitemap files those documents reference | | **Network origin** | AWS, no fixed IP allow-list available | ## What it does When initiated with a website origin URL, the Sitemap Importer makes a small, bounded set of HTTP GET requests to locate and read a site's sitemap. It tries the following URLs in sequence: * `https://{origin}/sitemap.xml` * `https://{origin}/sitemap_index.xml` * `https://{origin}/sitemap-index.xml` * `https://{origin}/robots.txt` (parsed only for `Sitemap:` directives, which are added to the candidate list) * any child sitemaps referenced by a sitemap index If you enter a URL ending in `.xml`, the importer also tries that URL directly. The first response that parses as a valid XML sitemap is used. The URLs it contains are returned to you for import. The importer does not store, cache, redistribute, or republish sitemap content. ## What it doesn't do * **Fetch HTML pages**, images, scripts, stylesheets, or any other assets * **Submit forms**, follow login flows, or access authenticated areas * **Run JavaScript** or use a headless browser * **Crawl recursively** beyond sitemap-index to child-sitemap links * **Persist** a long-running index of your site ## Request behavior Each import action triggers a small burst of requests scoped to a single origin. | | | | ----------------------- | ------------------------------------------------ | | **Per request timeout** | 10 seconds | | **Redirects** | Followed manually, up to 5 hops | | **Concurrency** | Sequential (not in parallel) | | **Caching** | None. Each user invocation issues fresh requests | ### Headers sent ```txt theme={null} User-Agent: ProfoundSheetsSitemapImporter/1.0 Accept: application/xml,text/xml,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8 ``` ### Volume and frequency Traffic from the Importer is driven entirely by customer activity. There's no continuous crawl. A typical interaction involves a handful of requests, after which the Importer doesn't contact an origin again until another import action targets the same site. ## `robots.txt` handling The importer reads `robots.txt` to discover sitemap URLs declared via `Sitemap:` directives. It doesn't evaluate `Disallow` rules against sitemap files, because the sitemap protocol treats those files as publicly discoverable. To prevent the importer from accessing your site, see [How to block](#how-to-block) section. ## How to identify the bot Use the exact User-Agent header: ```txt theme={null} User-Agent: ProfoundSheetsSitemapImporter/1.0 ``` All Profound bots use User-Agent strings that start with `Profound`, so a prefix match on `Profound` is a good way to identify all Profound traffic, current and future. ## How to block Block the importer using any of the following methods: * A WAF or CDN rule matching User-Agent equal to `ProfoundSheetsSitemapImporter/1.0` * A WAF or CDN rule matching User-Agent containing the prefix `Profound` * Returning a 4xx response to the bot's User-Agent on `/sitemap.xml`, `/sitemap_index.xml`, `/sitemap-index.xml`, and `/robots.txt` ## Reporting abuse If you observe behavior that doesn't match this documentation, such as requests for resources other than sitemap and `robots.txt` files, request rates that look like a crawl, or activity from an unrecognized `Profound*` User-Agent, report it to [security@tryprofound.com](mailto:security@tryprofound.com) with example log lines. Profound treats these reports as security issues and responds to all of them. # Citation Share for your owned domains Source: https://docs.tryprofound.com/cookbook/citations/citation-share Compute the headline owned-domain citation share plus the period-over-period delta — what the Profound Citation Share tile shows. The "Citation Share" tile in the Profound app shows the share of all citations pointing at domains you own, plus the change vs the previous window of equal length (the `+1.3% / -0.5%` delta badge). Same two-call pattern as Visibility's period-over-period: run the citations aggregate once for the current window and once for the prior window, diff the two shares. ## How this example works 1. **Look up owned domains** via `/categories/{id}/assets`. Keep rows flagged `is_owned`, collect their `website` + `alternate_domains`. 2. **Citations call** with `dimensions: ["root_domain"]` returns one row per cited domain. Sum `citation_share` across rows whose `root_domain` matches one of yours → headline value. 3. **Run the call twice** — current and prior window — and diff the two shares for the period-over-period delta. Most Profound categories scope citations to a `prompt_type` (the toggle in the UI). To match the default UI value, send `filters=[{"field": "prompt_type", "operator": "is", "value": "visibility"}]`. ```python Python theme={null} import os from datetime import date, timedelta from urllib.parse import urlparse from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" DAYS = 7 INCLUSIVE_END = date(2026, 5, 11) # the last day of your current window def normalize(host_or_url): """Strip scheme, www, and trailing slash so domains compare cleanly.""" p = urlparse(host_or_url if "://" in host_or_url else f"https://{host_or_url}") return (p.hostname or "").replace("www.", "").rstrip("/") def get_owned_domains(category_id): """All domains across assets flagged is_owned in the category.""" out = set() for a in client.organizations.categories.assets(category_id): if not a.is_owned: continue if a.website: out.add(normalize(a.website)) for d in a.alternate_domains or []: out.add(normalize(d)) return out def get_owned_citation_share(category_id, owned_domains, start, end): """Sum citation_share across all rows whose root_domain is one of yours.""" res = client.reports.citations( category_id=category_id, start_date=start.isoformat(), end_date=end.isoformat(), metrics=["count", "citation_share"], dimensions=["root_domain"], filters=[{"field": "prompt_type", "operator": "is", "value": "visibility"}], ) m = res.info.query["metrics"] d = res.info.query["dimensions"] i_share = m.index("citation_share") i_domain = d.index("root_domain") return sum( row.metrics[i_share] for row in res.data if normalize(row.dimensions[i_domain]) in owned_domains ) def current_and_prior_windows(inclusive_end, days): """Two (start, end_exclusive) pairs of equal length, back-to-back.""" current = ( inclusive_end - timedelta(days=days - 1), inclusive_end + timedelta(days=1), # +1 day → exclusive end ) prior = (current[0] - timedelta(days=days), current[0]) return current, prior # Helper — translate the category name to the UUID the report API expects. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") # Resolve name → ID, then run both windows. category_id = find_category_id(CATEGORY_NAME) owned = get_owned_domains(category_id) current, prior = current_and_prior_windows(INCLUSIVE_END, DAYS) current_share = get_owned_citation_share(category_id, owned, *current) prior_share = get_owned_citation_share(category_id, owned, *prior) delta_pp = (current_share - prior_share) * 100 print(f"Citation Share: {current_share:.1%} ({delta_pp:+.1f} pp vs prev period)") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here export CATEGORY_ID=your-category-uuid # 1) Owned domains curl "https://api.tryprofound.com/v1/org/categories/$CATEGORY_ID/assets" \ -H "X-API-Key: $PROFOUND_API_KEY" # 2a) Current window curl -X POST "https://api.tryprofound.com/v1/reports/citations" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["count", "citation_share"], "dimensions": ["root_domain"], "filters": [{"field": "prompt_type", "operator": "is", "value": "visibility"}] }' # 2b) Previous window (same body, prior dates) curl -X POST "https://api.tryprofound.com/v1/reports/citations" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-04-28", "end_date": "2026-05-05", "metrics": ["count", "citation_share"], "dimensions": ["root_domain"], "filters": [{"field": "prompt_type", "operator": "is", "value": "visibility"}] }' ``` # Citation Share and volume over time Source: https://docs.tryprofound.com/cookbook/citations/citation-share-over-time Build the daily owned-domain Citation Share line and the daily total citation volume — both from a single API call. The Citation Share tile has two views: a daily **share** line (your owned-domain Citation Share over time, with the previous-period overlay) and a daily **volume** count. Both come from the **same** API call — group by `["date", "root_domain"]`, request `count` and `citation_share`, then derive each series from the response. **Don't average daily rows to derive a period score** — they're computed differently. See [Conventions & gotchas](/cookbook/setup/conventions#don%E2%80%99t-average-daily-rows-to-get-a-period-score). ## How this example works 1. **Group by `["date", "root_domain"]`.** Each row is one (day, domain) pair with its own `count` and `citation_share`. 2. **Derive both series in one pass over the response**: * **Daily volume** = sum of `count` across all rows for each date. * **Daily owned-domain share** = sum of `citation_share` across rows whose `root_domain` is in your owned set, per date. (Per-AI-model averaged, same as the headline tile.) 3. **Repeat for the prior window** to get the overlay line. ```python Python theme={null} import os from datetime import date, timedelta from urllib.parse import urlparse from collections import defaultdict from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" DAYS = 7 INCLUSIVE_END = date(2026, 5, 11) # the last day of your current window def normalize(host_or_url): """Strip scheme, www, and trailing slash so domains compare cleanly.""" p = urlparse(host_or_url if "://" in host_or_url else f"https://{host_or_url}") return (p.hostname or "").replace("www.", "").rstrip("/") def get_owned_domains(category_id): """All domains across assets flagged is_owned in the category.""" out = set() for a in client.organizations.categories.assets(category_id): if not a.is_owned: continue if a.website: out.add(normalize(a.website)) for d in a.alternate_domains or []: out.add(normalize(d)) return out def get_daily_citations(category_id, owned_domains, start, end): """One call → both daily volume and daily owned-domain share. Returns [(date, volume, owned_share), ...] sorted by date. """ res = client.reports.citations( category_id=category_id, start_date=start.isoformat(), end_date=end.isoformat(), metrics=["count", "citation_share"], dimensions=["date", "root_domain"], date_interval="day", filters=[{"field": "prompt_type", "operator": "is", "value": "visibility"}], pagination={"limit": 50000, "offset": 0}, ) m = res.info.query["metrics"] d = res.info.query["dimensions"] i_count = m.index("count") i_share = m.index("citation_share") i_date = d.index("date") i_domain = d.index("root_domain") volume_by_day = defaultdict(int) owned_share_by_day = defaultdict(float) for row in res.data: day = row.dimensions[i_date] domain = normalize(row.dimensions[i_domain]) volume_by_day[day] += int(row.metrics[i_count]) if domain in owned_domains: owned_share_by_day[day] += row.metrics[i_share] return sorted( ( (day, volume_by_day[day], owned_share_by_day[day]) for day in volume_by_day ), key=lambda p: p[0], ) def current_and_prior_windows(inclusive_end, days): """Two (start, end_exclusive) pairs of equal length, back-to-back.""" current = ( inclusive_end - timedelta(days=days - 1), inclusive_end + timedelta(days=1), # +1 day → exclusive end ) prior = (current[0] - timedelta(days=days), current[0]) return current, prior def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") # Resolve name → ID, then run both windows. category_id = find_category_id(CATEGORY_NAME) owned = get_owned_domains(category_id) current, prior = current_and_prior_windows(INCLUSIVE_END, DAYS) current_rows = get_daily_citations(category_id, owned, *current) prior_rows = get_daily_citations(category_id, owned, *prior) print(f"{'Date':<12} {'Volume':>8} {'Owned share':>14}") print("Current period:") for day, volume, share in current_rows: print(f" {day:<10} {volume:>8} {share:>13.1%}") print("\nPrevious period:") for day, volume, share in prior_rows: print(f" {day:<10} {volume:>8} {share:>13.1%}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here # Current window curl -X POST "https://api.tryprofound.com/v1/reports/citations" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["count", "citation_share"], "dimensions": ["date", "root_domain"], "date_interval": "day", "filters": [{"field": "prompt_type", "operator": "is", "value": "visibility"}], "pagination": {"limit": 50000, "offset": 0} }' # Previous window — same body, prior dates. curl -X POST "https://api.tryprofound.com/v1/reports/citations" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-04-28", "end_date": "2026-05-05", "metrics": ["count", "citation_share"], "dimensions": ["date", "root_domain"], "date_interval": "day", "filters": [{"field": "prompt_type", "operator": "is", "value": "visibility"}], "pagination": {"limit": 50000, "offset": 0} }' ``` ## Common follow-ups The single-number KPI above the chart, plus its period-over-period delta. Every cited domain ranked by share, with the per-row pp delta column. # Citation Rank by domain (with deltas) Source: https://docs.tryprofound.com/cookbook/citations/top-citing-domains Build the Citation Rank leaderboard — domains sorted by share, with the per-row pp change vs the previous period. The "Citation Rank" / "Citation Share by Domain" panel in the Profound app is the citations endpoint grouped by `root_domain`, sorted by `citation_share` descending, with each row showing the change in share vs the previous window. Two calls (current + prior), merge by domain, diff the shares. ## How this example works 1. **`dimensions: ["root_domain"]`** — one row per cited domain in each call. Run both the current and prior windows. 2. **Merge by domain.** For each current-window row, look up the previous-window share (default 0 if missing) and diff. 3. **Sort by current `citation_share` descending** to match the UI. **`count` and `citation_share` measure different things.** `count` is the raw number of citations to that domain in the window. `citation_share` is `count / total_citations` **per AI model, then averaged across models**. A domain cited heavily on a low-volume model (e.g. Perplexity) can have a higher share than a domain with more total citations spread across high-volume models — the two columns won't sort in lockstep. Pick the one that matches the question you're answering and sort on it. ```python Python theme={null} import os from datetime import date, timedelta from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" DAYS = 7 INCLUSIVE_END = date(2026, 5, 11) # the last day of your current window TOP_N = 10 def get_domain_shares(category_id, start, end): """Return {domain: share} for every cited domain in the window.""" res = client.reports.citations( category_id=category_id, start_date=start.isoformat(), end_date=end.isoformat(), metrics=["count", "citation_share"], dimensions=["root_domain"], filters=[{"field": "prompt_type", "operator": "is", "value": "visibility"}], pagination={"limit": 50000, "offset": 0}, ) m = res.info.query["metrics"] d = res.info.query["dimensions"] i_count = m.index("count") i_share = m.index("citation_share") i_domain = d.index("root_domain") return { row.dimensions[i_domain]: { "count": row.metrics[i_count], "share": row.metrics[i_share], } for row in res.data } def current_and_prior_windows(inclusive_end, days): """Two (start, end_exclusive) pairs of equal length, back-to-back.""" current = ( inclusive_end - timedelta(days=days - 1), inclusive_end + timedelta(days=1), # +1 day → exclusive end ) prior = (current[0] - timedelta(days=days), current[0]) return current, prior def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") # Resolve name → ID, run current + prior, then build the leaderboard with deltas. category_id = find_category_id(CATEGORY_NAME) current, prior = current_and_prior_windows(INCLUSIVE_END, DAYS) current_domains = get_domain_shares(category_id, *current) prior_domains = get_domain_shares(category_id, *prior) rows = sorted( ( { "domain": name, "count": d["count"], "share": d["share"], "delta_pp": (d["share"] - prior_domains.get(name, {"share": 0})["share"]) * 100, } for name, d in current_domains.items() ), key=lambda r: r["share"], reverse=True, )[:TOP_N] for rank, r in enumerate(rows, start=1): sign = "+" if r["delta_pp"] >= 0 else "−" delta_str = f"{sign}{abs(r['delta_pp']):.1f} pp" print(f"{rank:>2}. {r['domain']:<32} {r['share']:>6.1%} {delta_str}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here # Current window curl -X POST "https://api.tryprofound.com/v1/reports/citations" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["count", "citation_share"], "dimensions": ["root_domain"], "filters": [{"field": "prompt_type", "operator": "is", "value": "visibility"}], "pagination": {"limit": 50000, "offset": 0} }' # Previous window — same body, prior dates. Merge by root_domain client-side # to compute the per-row share delta. curl -X POST "https://api.tryprofound.com/v1/reports/citations" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-04-28", "end_date": "2026-05-05", "metrics": ["count", "citation_share"], "dimensions": ["root_domain"], "filters": [{"field": "prompt_type", "operator": "is", "value": "visibility"}], "pagination": {"limit": 50000, "offset": 0} }' ``` ## Page-level breakdown Swap `root_domain` for `url` in `dimensions` to get the top citing **pages** (one row per URL) instead of the top citing domains. # Introduction Source: https://docs.tryprofound.com/cookbook/introduction End-to-end recipes for recreating the Profound dashboards with the public API. The Cookbook is a collection of **end-to-end recipes** that show how to recreate each chart, KPI, and table you see in the Profound app using only public API calls. Every recipe is self-contained: it lists the exact endpoints used, explains the request shape, and shows runnable Python and `curl` you can copy into your own code. If you just need the endpoint reference (every parameter, every metric), see the [REST API](/rest-api/introduction) tab. The Cookbook is the layer above that — common things people want to **build** with the API. ## Give your AI assistant context Building with Claude, ChatGPT, Cursor, or another AI coding assistant? Paste this URL — it's a compact index of every page in these docs that your assistant can fetch on demand: ``` https://docs.tryprofound.com/llms.txt ``` ## What you'll need Generate one in **Settings → API Keys** in the Profound app. See [Authentication](/rest-api/authentication) if you don't have one yet. Every report query is scoped to a category. See [Find your category ID](/cookbook/setup/find-your-category-id) to look one up by name. The recipes default to the Python SDK because it's the most common path. Install with `pip install profound`. Every recipe also has a `curl` tab if you'd rather hit the REST API directly. ## One-time setup Every recipe starts by constructing a client like this — you only need it once per script, even if you're chaining several recipes: ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here ``` ## Conventions used in every recipe Three things to internalize before you start — these come up in every recipe and trip up most new users: The API parses `end_date` at the start of day, so it **excludes** the date you send. To include all of `2026-05-10`, send `end_date="2026-05-11"`. Display the inclusive value to your users; send `+1 day` to the API. Every response includes `info.query.metrics` and `info.query.dimensions`, which echo back the exact order the API used when packing each row's `metrics` and `dimensions` arrays. ```python theme={null} order = res.info.query["metrics"] # e.g. ["visibility_score"] i = order.index("visibility_score") score = res.data[0].metrics[i] ``` Don't hardcode positions. The order may not match the order you requested. Period-over-period changes (the green/red `+1.0 pp` you see on every KPI tile) are computed client-side. Run the same call twice — once for the current window, once for the previous window of equal length — and diff the two scores in your code. ## Before you build Rate limits, exclusive end-dates, `info.query` ordering, errors, pagination. Read this once, paste into your AI assistant. How Categories, Topics, Prompts, Tags, Assets, and Personas relate. Every endpoint, its metrics/dimensions, and what it's for — one scannable page. ## Recipes ### Setup List the categories your key can see and pick one programmatically. Get every asset in a category with its `is_owned` flag and domains. ### Visibility An asset's score for the window plus the change vs the prior window. Build the daily / weekly / monthly line chart for an asset. Fetch both at once. Understand why they're different calls. Rank every asset in a category by any visibility metric. Multi-line chart of any hand-picked set of assets. Break a single asset's score down by the AI surface that answered. ### Citations Owned-domain share of all citations, with the period-over-period change. Daily owned-domain share line **and** daily total citation volume, both from one call. Every cited domain ranked by share, with the per-row pp delta column. # Conventions & gotchas Source: https://docs.tryprofound.com/cookbook/setup/conventions Read once, paste into your AI assistant, and skip the most common day-one mistakes. ## Authentication Every request needs your API key in the `X-API-Key` header. The Python SDK reads it from the `PROFOUND_API_KEY` env var. ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) categories = client.organizations.categories.list() ``` ```bash curl theme={null} curl https://api.tryprofound.com/v1/org/categories \ -H "X-API-Key: your_api_key_here" ``` Generate a key in the app under **Settings → API Keys**. Treat it like a password — it has full read access to your org's analytics data. ## Rate limit **600 requests per hour, per key.** Anything above returns `429 Too Many Requests`. Cache responses where you can and batch period-over-period or multi-asset queries instead of fanning out. ## `end_date` is exclusive — add one day `end_date` is parsed at the **start** of that day in **Eastern Time**, so it's excluded from the response. To include all of `May 10`, send `end_date="2026-05-11"`. | Window you want to display | What to send | | ------------------------------------ | ------------------------------------------------ | | `May 4 → May 10` (7 days, inclusive) | `start_date="2026-05-04", end_date="2026-05-11"` | | `April 1 → April 30` (full month) | `start_date="2026-04-01", end_date="2026-05-01"` | The `date_interval` buckets (`"day"` / `"week"` / `"month"`) are also computed in ET. ## Read column positions from `info.query`, not your request Each row in the response packs its `metrics` and `dimensions` as arrays. The order of values in those arrays comes from `info.query.metrics` and `info.query.dimensions` — **not** from the order you sent in the request. Always look it up: ```python theme={null} order = res.info.query["metrics"] i_score = order.index("visibility_score") score = res.data[0].metrics[i_score] ``` A response always looks like this: ```json theme={null} { "info": { "total_rows": 12345, "query": { "metrics": ["visibility_score", "share_of_voice"], "dimensions": ["asset_name"] } }, "data": [ { "metrics": [0.42, 0.17], "dimensions": [""] } ] } ``` ## Period-over-period deltas are client-side The API doesn't return change vs the previous period. Run the same call twice — current window and a prior window of equal length — and subtract. ## Don't average daily rows to get a period score A call with `dimensions=["date"]` returns one row per day. A call without `date` returns one row for the whole window. These are **different numbers**: the period score is traffic-weighted, an average of daily rows is not. Use the no-`date` call for headlines; use the with-`date` call for charts. Never derive one from the other. ## Pagination Default `pagination.limit` is `100`. Max is `50,000`. Use `info.total_rows` (returned on every response) to decide whether to paginate. Almost all queries fit in a single 50k page; only heavy `dimensions=["url", ...]` citation queries usually need a second page. ```python theme={null} pagination={"limit": 50000, "offset": 0} ``` If you do need more, increment `offset` by `limit` until you've covered `total_rows`. ## Filters Every report endpoint accepts a `filters` array of `{field, operator, value}` objects: ```json theme={null} { "field": "asset_name", "operator": "is", "value": "" } ``` | Operator | What it does | | --------------------------- | ------------------------------------ | | `is` | Exact match (scalar value) | | `not_is` | Not equal | | `in` | Match any value in an array | | `not_in` | Match none of the values in an array | | `contains` | Substring match (case-sensitive) | | `contains_case_insensitive` | Substring match (case-insensitive) | | `matches` | Regex match | `prompt_type` (with values like `"visibility"`) maps to the app's view toggles. Send `prompt_type=visibility` on Citations / Visibility queries to mirror the default UI scope. ## Error responses | Status | Meaning | What to check | | ------ | -------------------------- | --------------------------------------------------------- | | `400` | Validation error | The response body's `detail` / `errors` field. | | `401` | API key missing or invalid | The `X-API-Key` header; whether the key has been revoked. | | `403` | Key valid but no access | The `category_id` may belong to a different org. | | `404` | Wrong path | Typo or wrong API version. | | `429` | Rate limited | Back off; throttle to ≤600/hr. | | `5xx` | Server error | Retry with exponential backoff. | ## Timezones All bucketing happens in **Eastern Time**. A "last 7 days" range anchored to your local clock can land on a different ET day than you expect. Anchor scheduled jobs to ET: ```python theme={null} from datetime import datetime from zoneinfo import ZoneInfo today_et = datetime.now(ZoneInfo("America/New_York")).date() ``` # Data model Source: https://docs.tryprofound.com/cookbook/setup/data-model How everything in Profound nests together — from your Organization down to the slice dimensions on every report. Your **Organization** holds many **Categories**. Each Category contains its **Assets** (the brands you're tracking), grouped into **Topics**, made up of **Prompts**. Each Prompt then fans out across four dimensions — **Platform**, **Region**, **Persona**, and **Tag** — which is how reports slice the data. ## Entity reference | Entity | What it is | | ---------------- | --------------------------------------------------------------------------------------------------------- | | **Organization** | Your Profound account. Has many Categories. | | **Category** | A workspace — typically one brand and its competitive set. Every report query is scoped to one Category. | | **Asset** | A brand, company, or product tracked in the Category. Has `is_owned`, `website`, and `alternate_domains`. | | **Topic** | A grouping of related prompts (e.g. "Pricing", "Integrations"). | | **Prompt** | A question collected against AI models (e.g. "Best CRM for startups?"). | | **Platform** | The AI surface that answered (ChatGPT, Claude, Perplexity, …). Called `model` in API params. | | **Region** | The country the answer is scoped to (US, UK, Germany, …). | | **Persona** | An audience profile (e.g. "small business owner"). | | **Tag** | A free-form label applied to a Prompt. | ## What this means for queries 1. **Every report call is scoped to one `category_id`.** No cross-category queries. 2. **`filters` narrow which prompts contribute** — by `asset_name`, `topic_id`, `tag_id`, and the like. 3. **`dimensions` break results out by slice** — `model`, `region`, `persona`, or `tag`. ## Endpoints by entity | Entity | List endpoint | | --------------------- | ------------------------------------------------------------------------------------------- | | Categories | [`GET /v1/org/categories`](/api-reference/organization/get-categories) | | Topics | [`GET /v1/org/categories/{id}/topics`](/api-reference/organization/get-category-topics) | | Prompts | [`GET /v1/org/categories/{id}/prompts`](/api-reference/organization/list-prompts) | | Tags | [`GET /v1/org/categories/{id}/tags`](/api-reference/organization/get-category-tags) | | Assets | [`GET /v1/org/categories/{id}/assets`](/api-reference/organization/get-category-assets) | | Personas | [`GET /v1/org/categories/{id}/personas`](/api-reference/organization/get-category-personas) | | Platforms (org-level) | [`GET /v1/org/models`](/api-reference/organization/get-models) | | Regions (org-level) | [`GET /v1/org/regions`](/api-reference/organization/get-regions) | Click any endpoint above to jump to its full parameter reference. # Endpoints at a glance Source: https://docs.tryprofound.com/cookbook/setup/endpoints-at-a-glance Every Profound API endpoint, its key metrics/dimensions/filters, and what it's for — one scannable page. Orient yourself in 30 seconds. Each row is one endpoint; click through for the full parameter reference. For deeper context on conventions (dates, ordering, pagination), see [Conventions & gotchas](/cookbook/setup/conventions). ## Organization & setup | Method | Path | Use it for | | ------ | ---------------------------------- | ------------------------------------------------------ | | `GET` | `/v1/org/categories` | List every category your key can see. | | `GET` | `/v1/org/categories/{id}/assets` | Assets in a category, with `is_owned` + domains. | | `GET` | `/v1/org/categories/{id}/topics` | Configured topics for the category. | | `GET` | `/v1/org/categories/{id}/tags` | Configured tags. | | `GET` | `/v1/org/categories/{id}/personas` | Configured personas. | | `GET` | `/v1/org/regions` | Regions enabled on your org. | | `GET` | `/v1/org/models` | AI models (ChatGPT, Claude, etc.) enabled on your org. | | `GET` | `/v1/org/domains` | Domains your org owns. | | `GET` | `/v1/org/personas` | Personas enabled on your org. | ## Reports Answer Engine Insights report endpoints are `POST` requests scoped by `category_id` and return the `{info, data}` shape. Visibility, citations, sentiment, and query-fanouts reports take `category_id`, `start_date`, `end_date`, and `metrics`, plus report-specific optional fields. Traffic reports use a different shape: bots and referrals are scoped by `domain` rather than `category_id`. V2 FactCheck reports and V2 prompt answers are category-and-date scoped, but do not take a `metrics` field. ### `/v1/reports/visibility` How often your asset appears in AI answers. | Metrics | Dimensions | Common filters | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------- | | `visibility_score`, `share_of_voice`, `average_position`, `mentions_count`, `executions` | `date`, `asset_name`, `model`, `region`, `persona`, `topic`, `tag` | `asset_name`, `model_id`, `region_id`, `persona_id`, `topic_id`, `tag_id` | Common recipes: [Visibility Score (with delta)](/cookbook/visibility/asset-visibility-score), [over time](/cookbook/visibility/visibility-over-time), [leaderboard](/cookbook/visibility/leaderboard), [compare competitors](/cookbook/visibility/compare-competitors), [segment by model/region/persona](/cookbook/visibility/segment-by-model). ### `/v1/reports/citations` Which URLs and domains AI answers cite. | Metrics | Dimensions | Common filters | | ------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `count`, `citation_share` | `date`, `root_domain`, `url`, `citation_category`, `model`, `region`, `persona` | `prompt_type` (e.g. `"visibility"`), `model_id`, `region_id`, `persona_id` | Common recipes: [Citation Share + delta](/cookbook/citations/citation-share), [share + volume over time](/cookbook/citations/citation-share-over-time), [rank by domain](/cookbook/citations/top-citing-domains). `citation_share` is per-AI-model averaged. `count` is the raw number. Sorting by one doesn't track the other — pick the question you're answering and sort on the matching metric. ### `/v1/reports/sentiment` How positively/negatively AI answers reference your asset. | Metrics | Dimensions | Common filters | | ------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `positive`, `negative`, `occurrences` | `date`, `asset_name`, `theme`, `sentiment_type`, `topic`, `model`, `region`, `persona` | `asset_name`, `theme`, `topic_id`, `model_id`, `region_id`, `persona_id` | Numbers from the current public sentiment endpoint will not match the Profound app's headline value exactly. Sentiment v2 — which closes the gap — is releasing soon. ### `/v1/reports/query-fanouts` The query-fanout view (how prompts cascade across AI surfaces). Same `{info, data}` shape. ### `/v1/reports/referrals` (and `/v2/reports/referrals`) Human-referral traffic to your domains attributed back to AI answers. ### `/v1/reports/bots` (and `/v2/reports/bots`) AI crawler / bot traffic to your domains. ### `/v2/reports/factcheck` and `/v2/reports/factcheck/claims` FactCheck scores and inaccurate claims for a category. These endpoints take `category_id`, `start_date`, and `end_date`, but not `metrics`. ## Answers (per-prompt data) | Method | Path | Use it for | | ------ | --------------------- | --------------------------------------------------- | | `POST` | `/v1/prompts/answers` | Fetch raw AI answers for prompts in a category. | | `POST` | `/v2/prompts/answers` | Fetch raw per-execution answer rows for a category. | ## Agents | Method | Path | Use it for | | ------ | ------------------------------- | ----------------------- | | `GET` | `/v1/agents` | List configured agents. | | `GET` | `/v1/agents/{id}` | Get one agent's config. | | `POST` | `/v1/agents/{id}/runs` | Start a new agent run. | | `GET` | `/v1/agents/{id}/runs/{run_id}` | Poll a run's status. | ## Prompts | Method | Path | Use it for | | ------- | ---------------------------------------- | --------------------------- | | `GET` | `/v1/org/categories/{id}/prompts` | List prompts in a category. | | `POST` | `/v1/org/categories/{id}/prompts` | Add a prompt. | | `PATCH` | `/v1/org/categories/{id}/prompts` | Edit prompts. | | `PATCH` | `/v1/org/categories/{id}/prompts/status` | Enable/disable prompts. | ## Content Optimization | Method | Path | Use it for | | ------ | -------------------------------------------------- | ---------------------------------------- | | `GET` | `/v1/content/{asset_id}/optimization` | Optimization opportunities for an asset. | | `GET` | `/v1/content/{asset_id}/optimization/{content_id}` | One optimization in detail. | *** For the canonical parameter reference (every operator, every field, required vs optional), see the [REST API](/rest-api/introduction) tab. # Find your category ID Source: https://docs.tryprofound.com/cookbook/setup/find-your-category-id Every report call requires a category_id. Here's how to look one up programmatically. Every report endpoint (`/v1/reports/visibility`, `/v1/reports/citations`, `/v1/reports/sentiment`) requires a `category_id`. List the categories your key can see and pick the right one. ## How this example works 1. **One call lists everything.** No filters, no pagination. 2. **Match by name** if you know what you're looking for, or print them all if you don't. ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) categories = client.organizations.categories.list() # Print every category your key can see. for c in categories: print(f"{c.id} {c.name}") # Or: find one by name. TARGET = "" match = next((c for c in categories if c.name == TARGET), None) if match: print(f"\nFound Category '{TARGET}': {match.id}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here curl "https://api.tryprofound.com/v1/org/categories" \ -H "X-API-Key: $PROFOUND_API_KEY" ``` # List your owned assets and domains Source: https://docs.tryprofound.com/cookbook/setup/list-owned-assets Get every asset in a category, with the is_owned flag and any associated domains. Most "us vs. them" recipes need to know which assets in a category belong to you. This call returns every asset with its `is_owned` flag plus its website and alternate domains — everything you need for filtering and domain matching. ## How this example works 1. **One call per category.** Cache the result if you're going to make many report calls — assets change infrequently. 2. **Filter by `is_owned`** to get just your assets. 3. **Combine `website` + `alternate_domains`** to build the full set of domains you own (used by the [Citation Share recipe](/cookbook/citations/citation-share)). ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" def list_owned(category_id): """Return (owned_asset_names, owned_domains) for the category.""" names, domains = [], [] for a in client.organizations.categories.assets(category_id): if not a.is_owned: continue names.append(a.name) if a.website: domains.append(a.website) domains.extend(a.alternate_domains or []) return names, domains # Helper — translate the category name to the UUID the report API expects. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") # Resolve name → ID, then list owned assets and domains. category_id = find_category_id(CATEGORY_NAME) owned_names, owned_domains = list_owned(category_id) print("Owned assets:") for n in owned_names: print(f" - {n}") print("\nOwned domains:") for d in owned_domains: print(f" - {d}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here export CATEGORY_ID=your-category-uuid curl "https://api.tryprofound.com/v1/org/categories/$CATEGORY_ID/assets" \ -H "X-API-Key: $PROFOUND_API_KEY" ``` ## Companion lookups The same `/v1/org/categories/{id}/...` pattern works for other category resources: | Path | Returns | | ---------------------------------- | ------------------------------------ | | `/v1/org/categories/{id}/assets` | Assets in the category (used above) | | `/v1/org/categories/{id}/topics` | Topics covered by the category | | `/v1/org/categories/{id}/tags` | Tags configured for the category | | `/v1/org/categories/{id}/personas` | Personas configured for the category | | `/v1/org/categories/{id}/prompts` | Prompts the category runs against | # Visibility Score for one asset Source: https://docs.tryprofound.com/cookbook/visibility/asset-visibility-score Get an asset's traffic-weighted Visibility Score for a window and the change vs the prior window. The headline number on every Visibility tile in the Profound app, plus the `+1.0 pp` badge beside it. Same call twice — current window and prior window of equal length — and diff the two scores. ## How this example works 1. **Aggregate, not time-series.** Omit `date` from `dimensions`; filter on `asset_name`. Response has one row with a single traffic-weighted score. 2. **Run the same call twice** — once for the current window, once for the prior window of equal length — then subtract. 3. **Read positions from `info.query.metrics`.** Don't hardcode array indices. ```python Python theme={null} import os from datetime import date, timedelta from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" ASSET_NAME = "" METRIC = "visibility_score" # or share_of_voice, average_position, ... DAYS = 7 INCLUSIVE_END = date(2026, 5, 11) # the last day of your current window def get_visibility_score(category_id, asset_name, metric, start, end): """Aggregate score for one asset in one window.""" res = client.reports.visibility( category_id=category_id, start_date=start.isoformat(), end_date=end.isoformat(), metrics=[metric], filters=[{"field": "asset_name", "operator": "is", "value": asset_name}], ) order = res.info.query["metrics"] return res.data[0].metrics[order.index(metric)] def current_and_prior_windows(inclusive_end, days): """Two (start, end_exclusive) pairs of equal length, back-to-back.""" current = ( inclusive_end - timedelta(days=days - 1), inclusive_end + timedelta(days=1), # +1 day → exclusive end ) prior = (current[0] - timedelta(days=days), current[0]) return current, prior # Helpers — translate human-readable names into the IDs the report API needs. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") def find_asset_name(category_id, name): """Return the canonical asset name (case-insensitive) inside the category.""" for a in client.organizations.categories.assets(category_id): if a.name.lower() == name.lower(): return a.name raise ValueError(f"No asset named {name!r} in this category") # Resolve names → IDs, then run both windows. category_id = find_category_id(CATEGORY_NAME) asset_name = find_asset_name(category_id, ASSET_NAME) current, prior = current_and_prior_windows(INCLUSIVE_END, DAYS) current_score = get_visibility_score(category_id, asset_name, METRIC, *current) prior_score = get_visibility_score(category_id, asset_name, METRIC, *prior) delta_pp = (current_score - prior_score) * 100 print(f"{asset_name} {METRIC}: {current_score:.1%} ({delta_pp:+.1f} pp vs prev period)") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here # Current window curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "filters": [{"field": "asset_name", "operator": "is", "value": ""}] }' # Previous window — same body, prior dates. Diff the two scores client-side. curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-04-28", "end_date": "2026-05-05", "metrics": ["visibility_score"], "filters": [{"field": "asset_name", "operator": "is", "value": ""}] }' ``` ## Available metrics Swap `visibility_score` for any of these to get a different KPI tile: | Metric | What it is | Format | Direction | | ------------------ | ----------------------------------------------- | ------- | ------------------- | | `visibility_score` | Fraction of answers the asset appeared in | Percent | Higher is better | | `share_of_voice` | Fraction of all mentions the asset accounts for | Percent | Higher is better | | `average_position` | Average rank when the asset is mentioned | Rank | **Lower** is better | | `mentions_count` | Raw count of mentions | Number | Higher is better | | `executions` | Number of prompts run | Number | Higher is better | For `average_position`, flip the delta's color logic — a negative `delta` is an improvement. ## Common follow-ups Add `date` to dimensions to build a daily line chart. Why the headline call and the chart call return different numbers. # Compare your asset to competitors over time Source: https://docs.tryprofound.com/cookbook/visibility/compare-competitors Build a multi-line chart of Visibility Score for any list of assets. The multi-line "your asset vs competitors" chart in the Profound app is one call. Add `asset_name` to dimensions to get per-asset rows, and use an `in` filter to limit to the assets you care about. **Don't average daily rows to derive a period score** — they're computed differently. See [Conventions & gotchas](/cookbook/setup/conventions#don%E2%80%99t-average-daily-rows-to-get-a-period-score). ## How this example works 1. **`dimensions: ["date", "asset_name"]`** — one row per (date, asset). 2. **`filters` with `in` operator** — limit to a hand-picked set of assets. Without the filter you'd get every asset in the category. 3. **Pivot the response into one series per asset** before plotting. ```python Python theme={null} import os from collections import defaultdict from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" ASSETS = ["", "", "", ""] START_DATE = "2026-05-05" END_DATE = "2026-05-12" # exclusive — returns data through 2026-05-11 def get_competitor_series(category_id, asset_names, start_date, end_date, interval="day"): """Daily Visibility Score for each asset in `asset_names`. Returns {asset: [(date, score), ...]}.""" res = client.reports.visibility( category_id=category_id, start_date=start_date, end_date=end_date, metrics=["visibility_score"], dimensions=["date", "asset_name"], date_interval=interval, filters=[{"field": "asset_name", "operator": "in", "value": asset_names}], ) m_order = res.info.query["metrics"] d_order = res.info.query["dimensions"] i_score = m_order.index("visibility_score") i_date = d_order.index("date") i_asset = d_order.index("asset_name") by_asset = defaultdict(list) for row in res.data: by_asset[row.dimensions[i_asset]].append( (row.dimensions[i_date], row.metrics[i_score]) ) for asset in by_asset: by_asset[asset].sort(key=lambda x: x[0]) return by_asset # Helpers — translate human-readable names into the IDs the report API needs. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") def find_asset_names(category_id, names): """Return the canonical names for each input name (case-insensitive).""" by_lower = { a.name.lower(): a.name for a in client.organizations.categories.assets(category_id) } out = [] for n in names: if n.lower() not in by_lower: raise ValueError(f"No asset named {n!r} in this category") out.append(by_lower[n.lower()]) return out # Resolve names → IDs, then run. category_id = find_category_id(CATEGORY_NAME) canonical = find_asset_names(category_id, ASSETS) series_by_asset = get_competitor_series(category_id, canonical, START_DATE, END_DATE) # Print a labeled table: one row per asset, one column per day. print(f"Daily Visibility Score · {START_DATE} → {END_DATE} (exclusive)\n") dates = [d for d, _ in next(iter(series_by_asset.values()))] print(f"{'Asset':<14} " + " ".join(f"{d:>10}" for d in dates)) for asset, points in series_by_asset.items(): cells = " ".join(f"{s:>10.1%}" for _, s in points) print(f"{asset:<14} {cells}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "dimensions": ["date", "asset_name"], "date_interval": "day", "filters": [ {"field": "asset_name", "operator": "in", "value": ["", "", "", ""]} ] }' ``` ## Picking which competitors to plot To avoid hardcoding asset names, fetch the [top-N leaderboard](/cookbook/visibility/leaderboard) first, take the top 5 by score, and feed those names into the `in` filter on this call. # Headline score and the daily series, together Source: https://docs.tryprofound.com/cookbook/visibility/headline-and-daily Fetch both the period score and the daily line for one asset, and understand why they're different calls. A real dashboard tile shows two numbers: the **headline period score** (one big number) and the **daily series** (the line). These come from **two separate calls** to the same endpoint. This recipe runs both side-by-side and explains how each is extracted and why you can't derive one from the other. **Don't average daily rows to derive a period score** — they're computed differently. See [Conventions & gotchas](/cookbook/setup/conventions#don%E2%80%99t-average-daily-rows-to-get-a-period-score). ## How this example works 1. **Headline call** has no `date` dimension. The API returns a single traffic-weighted score for the whole window. 2. **Series call** adds `date` to dimensions and sets `date_interval`. The API returns one row per day, each containing that day's unweighted rate. 3. **Don't average the daily values to compute the headline.** Days with more prompts count more in the headline; the daily mean treats every day equally. The two are computed differently on purpose. ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" ASSET_NAME = "" START_DATE = "2026-05-05" END_DATE = "2026-05-12" # exclusive — returns data through 2026-05-11 def get_headline_score(category_id, asset_name, start_date, end_date): """Traffic-weighted period score (no date dim).""" res = client.reports.visibility( category_id=category_id, start_date=start_date, end_date=end_date, metrics=["visibility_score"], filters=[{"field": "asset_name", "operator": "is", "value": asset_name}], ) order = res.info.query["metrics"] return res.data[0].metrics[order.index("visibility_score")] def get_daily_series(category_id, asset_name, start_date, end_date): """One (date, score) point per day — for the line chart.""" res = client.reports.visibility( category_id=category_id, start_date=start_date, end_date=end_date, metrics=["visibility_score"], dimensions=["date"], date_interval="day", filters=[{"field": "asset_name", "operator": "is", "value": asset_name}], ) m_order = res.info.query["metrics"] d_order = res.info.query["dimensions"] i_score = m_order.index("visibility_score") i_date = d_order.index("date") points = [ (row.dimensions[i_date], row.metrics[i_score]) for row in res.data ] return sorted(points, key=lambda p: p[0]) # Helpers — translate human-readable names into the IDs the report API needs. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") def find_asset_name(category_id, name): """Return the canonical asset name (case-insensitive) inside the category.""" for a in client.organizations.categories.assets(category_id): if a.name.lower() == name.lower(): return a.name raise ValueError(f"No asset named {name!r} in this category") # Resolve names → IDs, then run both calls. category_id = find_category_id(CATEGORY_NAME) asset_name = find_asset_name(category_id, ASSET_NAME) headline_score = get_headline_score(category_id, asset_name, START_DATE, END_DATE) daily = get_daily_series(category_id, asset_name, START_DATE, END_DATE) print(f"Headline: {headline_score:.1%}\n") print("Daily:") for date, score in daily: print(f" {date} {score:.1%}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here # 1) Headline (no date dimension) curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "filters": [{"field": "asset_name", "operator": "is", "value": ""}] }' # 2) Daily series (add date dimension + date_interval) curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "dimensions": ["date"], "date_interval": "day", "filters": [{"field": "asset_name", "operator": "is", "value": ""}] }' ``` ## How the two responses differ | | Headline call | Series call | | --------------- | ---------------------------------- | -------------------------- | | `dimensions` | `[]` | `["date"]` | | `date_interval` | not sent | `"day"` (or week/month) | | Rows returned | 1 | N (one per bucket) | | Score per row | Traffic-weighted across the window | Unweighted within that day | | Used for | The big KPI number | The line on the chart | ## Why you can't average the daily values The headline weights days by their prompt volume — a day with 10,000 prompts moves the headline more than a day with 100. Averaging daily rates treats those two days as equal. For most assets the gap is 1-3 percentage points; for assets with bursty traffic it can be more. Always use the headline call for the headline. # Top-N leaderboard Source: https://docs.tryprofound.com/cookbook/visibility/leaderboard Rank every asset in a category by any visibility metric. The leaderboard on the Visibility tile is the same aggregate call as the headline — without the asset filter. The API returns one row per asset, and you sort client-side. ## How this example works 1. **Add `asset_name` to dimensions, drop the filter.** Now every asset gets its own row. 2. **Sort client-side.** The API returns rows in the order it computed them (typically sorted by the first metric descending); always re-sort in your code to be deterministic. Use `reverse=True` for "higher is better" metrics like `visibility_score`, and `reverse=False` for `average_position` (lower rank = better). 3. **`pagination.limit`.** The default is 100. Set it higher (max 50,000) if your category has many assets. ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" START_DATE = "2026-05-05" END_DATE = "2026-05-12" # exclusive — returns data through 2026-05-11 METRIC = "visibility_score" # or share_of_voice, average_position, ... TOP_N = 10 def get_leaderboard(category_id, metric, start_date, end_date): """All assets in the category ranked by the chosen metric.""" res = client.reports.visibility( category_id=category_id, start_date=start_date, end_date=end_date, metrics=[metric], dimensions=["asset_name"], pagination={"limit": 50000, "offset": 0}, ) m_order = res.info.query["metrics"] d_order = res.info.query["dimensions"] i_metric = m_order.index(metric) i_asset = d_order.index("asset_name") # Lower-is-better for average_position; higher-is-better for everything else. reverse = metric != "average_position" return sorted( [(row.dimensions[i_asset], row.metrics[i_metric]) for row in res.data], key=lambda x: x[1], reverse=reverse, ) # Helper — translate the category name to the UUID the report API expects. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") # Resolve name → ID, then run. category_id = find_category_id(CATEGORY_NAME) leaderboard = get_leaderboard(category_id, METRIC, START_DATE, END_DATE) for rank, (name, value) in enumerate(leaderboard[:TOP_N], start=1): print(f"{rank:>2}. {name:<24} {value:.1%}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "dimensions": ["asset_name"], "pagination": {"limit": 50000, "offset": 0} }' ``` ## Highlighting your owned assets To mark your own assets in the leaderboard (the "Owned" tag in the Profound UI), join the response against the [List your owned assets](/cookbook/setup/list-owned-assets) recipe — it returns the set of asset names flagged `is_owned` in this category. # Segment Visibility by model, region, or persona Source: https://docs.tryprofound.com/cookbook/visibility/segment-by-model Break a single asset's Visibility Score down by which AI model, region, or persona generated the answer. The Profound app's "Compare across models" view shows the same metric split by which AI surface served the answer. Same pattern works for region and persona. Add the segmentation field to `dimensions` (or use it as a `filter` to slice to one value). ## How this example works 1. **Add the segmentation field to `dimensions`** along with `asset_name`. You get one row per (segment, asset). 2. **Filter to your asset** so the response is one row per segment. 3. **Sort by score** to rank the segments. ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" ASSET_NAME = "" START_DATE = "2026-05-05" END_DATE = "2026-05-12" SEGMENT = "model" # or "region", "persona" def get_visibility_by_segment(category_id, asset_name, segment, start_date, end_date): """Visibility Score for one asset, broken down by `segment` (model/region/persona).""" res = client.reports.visibility( category_id=category_id, start_date=start_date, end_date=end_date, metrics=["visibility_score"], dimensions=[segment], filters=[{"field": "asset_name", "operator": "is", "value": asset_name}], ) m_order = res.info.query["metrics"] d_order = res.info.query["dimensions"] i_score = m_order.index("visibility_score") i_segment = d_order.index(segment) return sorted( [(row.dimensions[i_segment], row.metrics[i_score]) for row in res.data], key=lambda x: x[1], reverse=True, ) # Helpers — translate human-readable names into the IDs the report API needs. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") def find_asset_name(category_id, name): """Return the canonical asset name (case-insensitive) inside the category.""" for a in client.organizations.categories.assets(category_id): if a.name.lower() == name.lower(): return a.name raise ValueError(f"No asset named {name!r} in this category") # Resolve names → IDs, then run. category_id = find_category_id(CATEGORY_NAME) asset_name = find_asset_name(category_id, ASSET_NAME) rows = get_visibility_by_segment(category_id, asset_name, SEGMENT, START_DATE, END_DATE) print(f"{asset_name} Visibility Score by {SEGMENT}:") for segment, score in rows: print(f" {segment:<24} {score:.1%}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "dimensions": ["model"], "filters": [{"field": "asset_name", "operator": "is", "value": ""}] }' ``` ## Slicing to a single segment instead If you only want the score for one model (say, "where do I stand on ChatGPT specifically?"), drop `model` from `dimensions` and use it as a filter: ```python theme={null} filters=[ {"field": "asset_name", "operator": "is", "value": ""}, {"field": "model", "operator": "is", "value": "gpt-4o"}, ] ``` Now the response is one row — same shape as the [headline recipe](/cookbook/visibility/asset-visibility-score), scoped to ChatGPT. # Visibility over time Source: https://docs.tryprofound.com/cookbook/visibility/visibility-over-time Build the daily, weekly, or monthly Visibility line chart for one asset. The line chart on every Visibility tile in the Profound app is one call with the `date` dimension added. Use `date_interval` to switch between day, week, and month buckets. **Don't average daily rows to derive a period score** — they're computed differently. See [Conventions & gotchas](/cookbook/setup/conventions#don%E2%80%99t-average-daily-rows-to-get-a-period-score). ## How this example works 1. **Add `date` to dimensions and set `date_interval`** (`"day"` / `"week"` / `"month"`). One row per bucket. Different bucket sizes give different numbers — see [Headline + daily](/cookbook/visibility/headline-and-daily). 2. **Filter by asset** so each bucket is one row. 3. **Read positions from `info.query`** instead of hardcoding them. ```python Python theme={null} import os from profound import Profound client = Profound(api_key=os.environ["PROFOUND_API_KEY"]) # What to fetch — replace with your own values. CATEGORY_NAME = "" ASSET_NAME = "" START_DATE = "2026-05-05" END_DATE = "2026-05-12" # exclusive — returns data through 2026-05-11 DATE_INTERVAL = "day" # or "week" / "month" def get_visibility_over_time(category_id, asset_name, start_date, end_date, interval="day"): """Daily/weekly/monthly Visibility Score for one asset, sorted by date.""" res = client.reports.visibility( category_id=category_id, start_date=start_date, end_date=end_date, metrics=["visibility_score"], dimensions=["date"], date_interval=interval, filters=[{"field": "asset_name", "operator": "is", "value": asset_name}], ) m_order = res.info.query["metrics"] d_order = res.info.query["dimensions"] i_score = m_order.index("visibility_score") i_date = d_order.index("date") points = [ (row.dimensions[i_date], row.metrics[i_score]) for row in res.data ] points.sort(key=lambda p: p[0]) return points # → [("2026-05-05", 0.78), ("2026-05-06", 0.81), ...] # Helpers — translate human-readable names into the IDs the report API needs. def find_category_id(name): """Return the UUID of the category whose name matches (case-insensitive).""" for c in client.organizations.categories.list(): if c.name.lower() == name.lower(): return c.id raise ValueError(f"No category named {name!r}") def find_asset_name(category_id, name): """Return the canonical asset name (case-insensitive) inside the category.""" for a in client.organizations.categories.assets(category_id): if a.name.lower() == name.lower(): return a.name raise ValueError(f"No asset named {name!r} in this category") # Resolve names → IDs, then run. category_id = find_category_id(CATEGORY_NAME) asset_name = find_asset_name(category_id, ASSET_NAME) points = get_visibility_over_time(category_id, asset_name, START_DATE, END_DATE, DATE_INTERVAL) for date, score in points: print(f"{date} {score:.1%}") ``` ```bash curl theme={null} export PROFOUND_API_KEY=your_api_key_here curl -X POST "https://api.tryprofound.com/v1/reports/visibility" \ -H "X-API-Key: $PROFOUND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "category_id": "your-category-uuid", "start_date": "2026-05-05", "end_date": "2026-05-12", "metrics": ["visibility_score"], "dimensions": ["date"], "date_interval": "day", "filters": [ {"field": "asset_name", "operator": "is", "value": ""} ] }' ``` ## Switching the bucket Swap `date_interval` to roll the data up to coarser buckets: | `date_interval` | What you get | | --------------- | ----------------------------------------- | | `"day"` | One point per calendar day (Eastern Time) | | `"week"` | One point per ISO week | | `"month"` | One point per calendar month | The number of returned rows scales with the bucket size for the same window. # AEM Integration Setup Guide Source: https://docs.tryprofound.com/integrations/adobe-experience-manager/setup/aem-setup-guide This guide walks you through connecting Adobe Experience Manager (AEM) as a Cloud Service to Profound using an Adobe OAuth Server-to-Server credential. Once connected, Profound Agents can access, create, and update the Content Fragment models, folders, and content allowed by the credential's AEM permissions. ## Before you start You'll need: * An [AEM as a Cloud Service](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/overview/introduction) program and **Author** environment * [AEM Cloud Manager](https://experience.adobe.com/#/cloud-manager) access to commit to an Adobe-managed repository and run a config pipeline (**Deployment Manager** role required) * Permissions to manage product profiles in the [Adobe Admin Console](https://adminconsole.adobe.com/) and to create OAuth credentials in the [Adobe Developer Console](https://developer.adobe.com/console) * A [Profound](https://www.tryprofound.com/) account ## Setup guide * Open [Adobe Cloud Manager](https://experience.adobe.com/#/cloud-manager). * Sign in under the organization that owns the AEM program you intend to use with Profound. * Select and open the program, then select **Environments** in the left sidebar. * Open the environment you intend to use and copy its base **Author URL**. It should look like: `https://author-p12345-e67890.adobeaemcloud.com`. Adobe Cloud Manager program overview page screenshot with Environments menu option and Author URL highlighted To enable API access in your environment, follow [Adobe's AEM APIs setup guide](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/setup). Once done, confirm that you have the correct product profile to use with the AEM API: * Go back to your AEM program environment page in [Adobe Cloud Manager](https://experience.adobe.com/#/cloud-manager) (where you left off in [Step 1](#step-1)). * Open the environment's action menu and select **Manage Access** > **Author Profiles**. [Adobe Admin Console](https://adminconsole.adobe.com/) opens your environment's page. Adobe Cloud Manager environment action menu screenshot with Manage Access and Author Profiles options highlighted * Confirm that the **AEM Sites Content Managers - author...** profile exists for your program and environment. Adobe Admin Console Product profiles list screenshot with the AEM Sites Content Managers author profile highlighted To authenticate with Profound, create an OAuth credential. You, or anyone with a **Developer** role in your **AEM Sites Content Managers** product profile, can create it. To assign a **Developer** role to the credential creator, continue where you left off in [Step 2](#step-2): * On your environment's page in [Adobe Admin Console](https://adminconsole.adobe.com/), select the **AEM Sites Content Managers - author...** product profile. * Open the **Developers** tab, then select **Add developers**. Adobe Admin Console product profile screenshot with the Developers tab and Add developers button highlighted * Add the email or username of the person creating the OAuth credential, then select **Save**. Learn more about assigning developers to Adobe product profiles in Adobe's [Manage developers in Adobe Admin Console](https://helpx.adobe.com/enterprise/using/manage-developers.html) guide. * Open the [Adobe Developer Console](https://developer.adobe.com/console). If you're following these steps in sequence, you should already be signed in under the organization that owns your Author AEM environment (same as in [Step 1](#step-1)). * Select **Create new project** in the **Quick start** section, or go to **Projects** in the top navigation menu and select **Create new project**. Adobe Developer Console home page quickstart section screenshot with Create new project option highlighted * On the project page, select **Edit project**, give your project a descriptive name (such as **Profound integration credentials**), then select **Save**. Adobe Developer Console Edit Project dialog screenshot with the Edit project button and the Project Title field highlighted * Select **Add to Project** > **API**. Adobe Developer Console project page screenshot with Add to Project and the API option highlighted * On the **Add an API** page, filter the API list by **Experience Cloud**, select **AEM CS Sites Content Management**, then select **Next**. Add an API view screenshot with Experience Cloud filter option, AEM CS Sites Content Management option, and Next button highlighted * Select **Server-to-Server** > **OAuth Server-to-Server** as the authentication type. Configure API dialog screenshot with Server-to-Server Authentication and Next highlighted Configure API dialog screenshot with OAuth Server-to-Server, the Credential name field, and Next button highlighted * Select the Author product profile you prepared in [Step 2](#step-2), then save the configuration. Configure API dialog screenshot with the AEM Sites Content Managers product profile selected and Save configured API option highlighted The AEM Sites Content Management API also provides the folder access Profound uses. No additional credentials are required. After you save the API configuration, [Adobe Developer Console](https://developer.adobe.com/console) returns you to the project page. In the **Credentials** section, select your new OAuth Server-to-Server credential. From the credential page, copy the values you need to create the AEM integration in Profound: * **Client ID** * **Client Secret** * **Scopes** — copy the complete value exactly as Adobe displays it * **Organization ID** — optional; copy it only if the AEM tenant requires the `x-gw-ims-org-id` header OAuth Server-to-Server credential details view screenshot with Client ID, Client Secret, Scopes, and Organization ID highlighted Before you can use the credential, register its client ID with your Author environment. * Back in [Adobe Cloud Manager](https://experience.adobe.com/#/cloud-manager), open the program and select **Repositories** in the left sidebar. * Identify the Adobe-managed repository and branch used by your environment's config pipeline. Adobe Cloud Manager Repositories screenshot with Repositories menu option, repository name, and the configured pipeline branch highlighted * Select **Access Repo Info** and copy the repository URL and access credentials. * Using your regular development workflow, clone the repository onto your machine and create or update the `config/api.yaml` file in the root directory with the following configuration: ```yaml theme={null} kind: "API" version: "1" metadata: envTypes: ["dev", "stage", "prod"] data: allowedClientIDs: author: - "" ``` Replace `` with the client ID you copied from Developer Console in [Step 5](#step-5). If you already have other API credentials configured, keep the existing configuration and add the client ID to the `author` list under `allowedClientIDs`. Learn more about managing repositories and configuring pipelines in the following Adobe resources: * [AEM API client ID registration guide](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/developing/open-api-based-apis#registering-a-client-id) * [Manage repositories in Cloud Manager](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/using-cloud-manager/managing-code/managing-repositories) * [Use config pipelines](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/config-pipeline) guide * [Invoke OpenAPI-based AEM APIs using OAuth Server-to-Server authentication](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/invoke-api-using-oauth-s2s) tutorial * Still on your program page in [Adobe Cloud Manager](https://experience.adobe.com/#/cloud-manager), select **Pipelines** in the left sidebar. * Select **Add Pipeline**, then select **Add Non-Production Pipeline** for the development environment, or **Add Production Pipeline** for stage and production. Adobe Cloud Manager Pipelines screenshot with the Pipelines menu option and Add Pipeline button highlighted * In the **Add Pipeline** dialog, select **Deployment Pipeline**, give your pipeline a descriptive name, then select **Continue**. * On the next screen, fill in the values as follows: * Select code to deploy: **Targeted deployment** * Include: **Config** * Eligible Deployment Environments: select your AEM environment from the dropdown * Repository, Git Branch, Code Location: the repository, branch, and config directory from [Step 6](#step-6) * Select **Save**. Your new config pipeline appears in the list of pipelines in the **Pipelines** section. Select the pipeline, then select **Run selected**. Wait for the deployment to complete successfully. Adobe Cloud Manager Pipelines screenshot with a new config pipeline selected and a Run selected button highlighted Learn more about deploying config pipelines in the following Adobe resources: * [Use config pipelines](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/config-pipeline) guide * [Cloud Manager CI/CD pipelines](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/using-cloud-manager/cicd-pipelines/introduction-ci-cd-pipelines) overview * In [Profound](https://platform.tryprofound.com), go to **Integrations** in the left sidebar. * Search for **Adobe Experience Manager (S2S)** and select it. Profound Integrations view screenshot with Adobe Experience Manager (S2S) integration card highlighted * In the Adobe Experience Manager (S2S) integration view, select **+ Add account**. * In the **Connect Adobe Experience Manager (S2S)** dialog, enter the Author URL from [Step 1](#step-1) and the client secret, client ID, scopes, and organization ID you retrieved in [Step 5](#step-5). * Select **Connect account**. If the connection succeeds, your AEM environment appears in the list of connected accounts. Profound Adobe Experience Manager integration view screenshot with a connected account highlighted Revoke Profound's access to your AEM environment at any time by selecting **Revoke** in the integration account action menu. Profound connected account action menu screenshot with the three-dot menu and the Revoke option highlighted To make sure Profound has all the permissions it needs to work with your AEM environment, verify that the AEM Agent nodes run successfully. * In [Profound](https://platform.tryprofound.com), [create a new Agent](https://help.tryprofound.com/articles/2212787792-create-an-agent), or open an existing Agent draft. * Add the Adobe Experience Manager **List Content Fragments** and **Get Content Fragment** nodes, then select the connected AEM instance. * Select **Test node** (a play icon) in the node configuration panel and confirm that the AEM output contains the expected data. List Content Fragments node configuration panel screenshot with the play icon highlighted * If you intend to create or update content fragments with Profound Agents, test the **Create Content Fragment** and **Update Content Fragment** nodes the same way. Delete test content directly in the AEM environment. Current Profound Agent nodes don't include a delete operation. ## Rotating AEM credentials ### Rotate a client secret only Adobe supports two client secrets on one OAuth Server-to-Server credential, which allows rotation without changing the client ID. Rotate your client secret as instructed in [Adobe developer documentation](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/implementation#rotating-client-secrets), then update the credentials in Profound (**Integrations** > **Adobe Experience Manager (S2S)** > **Edit credentials** in a connected account's action menu). The client ID doesn't change during this process, so you don't need to update or redeploy `api.yaml`. ### Rotate the entire OAuth credential To replace the entire AEM OAuth credential: 1. Generate a new credential as described in [Step 4](#step-4) of the setup guide. 2. Deploy the new client ID in `config/api.yaml` as instructed in [Step 6](#step-6) of the setup guide. 3. Update the credentials in Profound. Keep the old client ID registered until you've verified the new connection. ## Best practices * Use an **Author** environment only. Profound doesn't connect to AEM Publish or Preview. Learn more about environments in [AEM as a Cloud Service tutorials](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/cloud-manager/environments). * Test the read and write Profound Agent nodes separately to verify their folder and repository permissions. * Always store your client secret and other credentials in an approved secret manager, and never include them in screenshots, tickets, or source control. # Troubleshooting AEM Integration Setup Source: https://docs.tryprofound.com/integrations/adobe-experience-manager/setup/aem-setup-troubleshooting ## OAuth Server-to-Server authentication method is not available Confirm that the credential creator is assigned as a **Developer** on the intended product profile and that the profile includes the AEM Sites service, as described in [Step 2](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-2) and [Step 3](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-3) of the setup guide. ## AEM CS Sites Content Management product profile is missing Go back to [Step 2](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-2) of the setup guide and verify the following: * Your Adobe organization owns the AEM environment you intend to use with Profound. * The environment is modernized for API access as instructed in Adobe's [Set up OpenAPI-based AEM APIs](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/setup) guide. ## Profound can't save the AEM credentials Confirm the following: * All connection values are correct and match the values of your OAuth credential from [Step 5](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-5) of the setup guide. * Your OAuth credential uses the **AEM Sites Content Managers - author...** product profile. Check this in the [Adobe Developer Console](https://developer.adobe.com/console). If it doesn't, repeat [Step 2](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-2) of the setup guide. * The config pipeline that deploys your API configuration (`api.yaml`) ran successfully. Check the **Pipelines** section of your AEM program page in [Adobe Cloud Manager](https://experience.adobe.com/#/cloud-manager). If it didn't, repeat [Step 6](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-6) and [Step 7](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-7) of the setup guide. * The client ID of the OAuth credential is in the correct place in the `api.yaml` configuration file, as instructed in [Step 6](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-6) of the setup guide. * An Author IP allowlist isn't blocking Profound. Learn more in Adobe's [Manage IP Allow Lists](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/using-cloud-manager/ip-allow-lists/managing-ip-allow-lists) guide. If you have to apply an Author IP allowlist, contact Profound Support to obtain the current Profound outbound IP addresses rather than reusing values from an old guide or ticket. * If you're using a sandbox environment, it's online and not hibernated. Learn more in Adobe's [Hibernate and De-Hibernate Sandbox Environments](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/using-cloud-manager/programs/hibernating-environments) guide. ## Content Fragment Models don't appear in Profound This usually means either the Content Fragment Model isn't allowed for the folders you use with Profound, or your OAuth credential's technical account lacks the permissions to manage Content Fragment Model configuration. Confirm the following: * The Content Fragment Model is enabled and allowed on your assets folder. Learn more in Adobe's [Content Fragment Models](https://experienceleague.adobe.com/en/docs/experience-manager-65/content/assets/content-fragments/content-fragments-models#enabling-disabling-a-content-fragment-model) documentation. * The technical account's service group has permissions to manage the Content Fragment Model configuration. With the setup described in the [setup guide](/integrations/adobe-experience-manager/setup/aem-setup-guide#setup-guide), your technical account should already have the necessary permissions to read and manage your AEM content. If it doesn't, manage permissions for specific folders and files in your AEM environment as described in Adobe's [Product Profile and Services user group permission management](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/how-to/services-user-group-permission-management) guide. Grant the AEM OAuth credential's technical account the narrowest permissions that support the Agent nodes you intend to use in Profound. Scope create and update access to approved folders only. ## Folders don't appear in Profound * Go back to [Step 5](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-5) of the setup guide and confirm that the **Scopes** connection value for your Profound AEM integration matches the scopes value of your AEM OAuth credential. * Confirm that the scopes value includes `aem.fragments.management` and `aem.folders`. If it doesn't, don't add the missing scopes by hand. Confirm instead that you completed all prerequisites in [Step 2](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-2) and [Step 3](/integrations/adobe-experience-manager/setup/aem-setup-guide#step-3) of the setup guide. * Confirm that the technical account's service group has the permissions it needs to manage content in the `/content/dam` folder in your AEM environment. To add permissions for specific folders and files, follow Adobe's [Product Profile and Services user group permission management](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/how-to/services-user-group-permission-management) guide. ## Create or Update Content Fragment node returns a permissions error This usually means your AEM OAuth credential is authenticated but doesn't have the required repository access. Follow Adobe's [Product Profile and Services user group permission management](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/how-to/services-user-group-permission-management) guide to grant the service group that contains your AEM technical account the create or update permissions it needs for the folder you use with Profound. # Using Adobe Experience Manager in Agents Source: https://docs.tryprofound.com/integrations/adobe-experience-manager/using-adobe-experience-manager-in-agents After you connect your Adobe Experience Manager (AEM) account, the following nodes become available in Profound Agents. Use them to find, read, create, and update AEM Content Fragments. All operations run against the connected AEM Author environment and use the permissions assigned to the OAuth credential's technical account. ## Available nodes ### List Content Fragments Retrieve Content Fragments from the connected Author environment. #### Required inputs * **AEM Instance** — the connected Author environment #### Optional inputs * **Folder** — limits the list to an approved folder below `/content/dam` * **References** — controls whether linked fragments are returned and hydrated * **Limit** — number of results to return; the default is `25` and the maximum is `50` * **Cursor** — a pagination cursor from a previous List or Search node's output, used to fetch the next page of results * **Projection** — use `summary` when you need summary data only #### Output The output contains the matching Content Fragments and, when more results are available, a pagination cursor for the next page. ### Search Content Fragments Find Content Fragments matching one or more criteria. #### Required inputs * **AEM Instance** — the connected Author environment Profound starts the search with a **Folder** filter of `/content/dam`. Change that folder or add another filter as needed. #### Search filters * **Search text** — searches the AEM-supported fragment content for the selected text * **Folder** — scopes the search to a DAM folder path * **Models** — limits results to selected Content Fragment Models * **Statuses** — New, Draft, Published, Modified, or Unpublished * **Locale** — limits results to a locale * **Direct children only** — excludes fragments in nested folders * **Created after** and **Created before** — constrain the creation date #### Optional inputs * **Raw Query** — an AEM-supported search-query JSON; use it for filters or sorting the node configuration form doesn't include * **Limit** — number of results to return; the default is `25` and the maximum is `50` * **Cursor** — a pagination cursor from a previous List or Search node's output, used to fetch the next page of results * **Projection** — use `summary` when you need summary data only #### Output The output contains the matching Content Fragments and, when more results are available, a pagination cursor for the next page. ### Get Content Fragment Retrieve the complete data for one Content Fragment. #### Required inputs * **AEM Instance** — the connected Author environment * **Content Fragment** — select a fragment from the dropdown, or use a variable from a previous node's output #### Optional inputs * **Content Fragment ID** — the fragment UUID as text, which overrides the fragment selected in [required inputs](#required-inputs) * **References** — controls whether linked fragments are returned and hydrated #### Output The output includes the Content Fragment data and its ETag when available. Model-specific fields follow the response AEM returns. ### Create Content Fragment Add Author content based on an enabled Content Fragment Model. #### Required inputs * **AEM Instance** — the connected Author environment * **Parent Folder** — the destination folder below `/content/dam` * **Content Fragment Model** — the model that defines the fragment fields * **Title** — the fragment title * **Data format** * **Structured** — loads the selected model and displays its fields in the configuration panel * **Raw JSON** — accepts an object whose keys match the AEM model's field names * **Fields** / **Raw JSON** — the structure for your input data, based on the selected **Data format** Use Raw JSON when the Content Fragment Model is supplied with a variable, because the node editor can't load that model's field definition in advance. #### Optional inputs * **Name** — URL-safe Content Fragment name; derived from the **Title** when left blank * **Description** — fragment description #### Output The output includes the created Content Fragment, its ETag, and its Location when AEM provides those values. Creating or updating a fragment doesn't publish it. Review and publish the content in AEM. ### Update Content Fragment Change an existing fragment. The node modifies only the fields you provide. #### Required inputs * **AEM Instance** — the connected Author environment * **Content Fragment** — select a fragment from the dropdown, or use a variable from a previous node's output * **Data format** * **Structured** — loads the selected model and displays its fields in the configuration panel * **Raw JSON** — accepts an object whose keys match the AEM model's field names * At least one change to **Title**, **Description**, or **Fields** / **Raw JSON** Use Raw JSON when the Content Fragment Model is supplied with a variable, because the node editor can't load that model's field definition in advance. #### Optional inputs * **Title** — leave empty to keep the current title * **Description** — leave empty to keep the current description * **Fields** / **Raw JSON** — the structure for your input data, based on the selected **Data format** * **References** — controls whether linked fragments are returned and hydrated * **ETag** — optimistic-lock value from an earlier Get or Update output. When left blank, Profound reads the current ETag immediately before sending the update. If the provided ETag is stale, the node reports a conflict. #### Output The output includes the updated Content Fragment and its new ETag when AEM provides one. Creating or updating a fragment doesn't publish it. Review and publish the content in AEM. *** ## Key concepts ### AEM Instance The connected AEM Author environment. Select it before choosing a model, folder, or fragment. ### Content Fragment Model The AEM schema that defines a fragment's fields, types, required values, and allowed content. The model must be enabled and allowed by the destination folder's policy. ### DAM paths Content Fragment folders use repository paths below `/content/dam`. Folder permissions and policies control which content Profound can read or change. ### Content Fragment ID AEM API operations identify a Content Fragment by UUID. Profound's picker displays searchable fragment information and supplies the UUID to the node. ### ETag An ETag is a unique identifier for a specific version of a Content Fragment's data. Pass it from a **Get Content Fragment** node to the **Update Content Fragment** node to prevent the workflow from silently overwriting a change made after the fragment was read. ### Pagination List and Search nodes return up to `50` fragments per request. When the output contains a cursor, provide it to the next List or Search node to retrieve the next page of results. ### Hydrated Content Fragment A Content Fragment that contains its full field values, rather than only basic IDs and URLs. ### References References control how linked fragments are included. Hydrated and all-reference modes can return larger responses, so use them only when the workflow needs linked content. *** ## Troubleshooting ### A folder, fragment, or model picker is disabled or empty Select an **AEM Instance** first. If the picker stays empty, verify that your OAuth credential's technical account can read the corresponding model, folder, or fragment. Learn more in the [setup troubleshooting](/integrations/adobe-experience-manager/setup/aem-setup-troubleshooting) guide. ### Search returns an invalid-query error Restore the **Folder** filter to `/content/dam` or add another filter. When using **Raw Query**, include a non-empty JSON object and use Adobe-supported filter and sort fields. ### Create node reports an unknown field or invalid value Select the model and use **Structured** format when possible. If you have to use **Raw JSON** format, make sure each key matches the AEM model field name and provide a value with the model's expected type and cardinality. ### Update node reports that there are no changes Enter at least one **Title**, **Description**, or model field value. Empty inputs preserve the current content. ### Update node reports a conflict The fragment changed after AEM issued the ETag. Run the **Get Content Fragment** node again, review the latest content, then apply the update with the new ETag. ### A node returns a permissions error Confirm that the technical account's service group has permission for that exact operation and DAM path. Read access can succeed while Create or Update remains blocked. To add permissions for specific folders and files in your AEM environment, follow Adobe's [Product Profile and Services user group permission management](https://experienceleague.adobe.com/en/docs/experience-manager-learn/cloud-service/aem-apis/openapis/how-to/services-user-group-permission-management) guide. ### Only the first page of results is returned Use the cursor returned by the List or Search node as the next request's **Cursor**. Each page can contain no more than `50` results. *** ## Additional resources * [AEM Sites Content Management API reference](https://developer.adobe.com/experience-cloud/experience-manager-apis/api/stable/sites/) * [AEM Folders API reference](https://developer.adobe.com/experience-cloud/experience-manager-apis/api/stable/folders/) # Connect Contentful to Profound Source: https://docs.tryprofound.com/integrations/contentful/connect-contentful-to-profound ## Prerequisites Before connecting Contentful to Profound, ensure: * You have access to your Contentful organization * You have **Admin** or **Owner** permissions for the space you want to connect * Your Contentful account is active ## Connect Your Account 1. In Profound, go to **Account → Integrations → Contentful**. 2. Click **Connect account**. 3. You'll be redirected to Contentful to authorize Profound. 4. Sign in to your Contentful account and grant the requested permissions. 5. Select the organization and space you want to connect. 6. Once authorized, you'll be redirected back to Profound. Once connected, Contentful will appear as an available integration and can be selected inside Agents. Profound requests **content\_management\_manage** scope, which allows creating, updating, and publishing content in your connected spaces. # Using Contentful in Agents Source: https://docs.tryprofound.com/integrations/contentful/using-contentful-in-agents After connecting your Contentful account, the following actions become available as Agent steps. Each step requires you to select a connected **Contentful Account** from a dropdown. #### **Create Entry** Create a new entry in your Contentful space. **Required inputs** * **Contentful Account** * **Space** * **Environment** * **Content Type** * **Fields** (dynamic based on content type schema) **Optional inputs** * **Locale** (defaults to space's default locale) * **Publish** (publish immediately or save as draft) The entry is created based on your content type's schema. Rich text fields accept Markdown which is automatically converted to Contentful's rich text format. **Tip:** Leave **Publish** unchecked to create drafts for review before publishing. #### **Update Entry** Update an existing entry in your Contentful space. **Required inputs** * **Contentful Account** * **Space** * **Environment** * **Entry ID** **Optional inputs** * **Fields** (only fields you want to update) * **Locale** (defaults to space's default locale) * **Publish** (publish changes immediately) **Important:** Only the fields you provide will be modified; all other fields remain unchanged. #### **Get Entry** Retrieve a single entry by ID. **Required inputs** * **Contentful Account** * **Space** * **Environment** * **Entry ID** The output includes the entry's full content with all fields and metadata. #### **List Entries** Retrieve a list of entries from your Contentful space. **Required inputs** * **Contentful Account** * **Space** * **Environment** **Optional inputs** * **Content Type** (filter by content type) * **Limit** (number of entries to return) * **Skip** (pagination offset) The output is a structured list of entries that can be used in downstream Agent steps. *** ## Supported Field Types Contentful's dynamic schema means Profound automatically adapts to your content model. All field types are supported: | Field Type | Description | Input Format | | --------------- | ---------------------- | ------------------------- | | **Short text** | Single-line text | Plain text | | **Long text** | Multi-line text | Plain text | | **Rich text** | Formatted content | Markdown (auto-converted) | | **Number** | Decimal numbers | Number | | **Integer** | Whole numbers | Integer | | **Boolean** | True/false | Boolean | | **Date & time** | ISO 8601 dates | Date string | | **Location** | Geographic coordinates | `{lat, lon}` object | | **JSON object** | Arbitrary JSON | JSON object | | **Media** | Asset references | Asset ID | | **Reference** | Entry references | Entry ID or array of IDs | *** ## Working with Rich Text Rich text fields in Contentful use a structured document format (AST). Profound accepts **Markdown**, **HTML**, or **Contentful's native rich text JSON** format. **Supported Markdown:** * Headings (`# H1` through `###### H6`) * Paragraphs * **Bold**, *italic*, and other formatting * Bullet and numbered lists * Blockquotes * Code blocks * Links **Supported HTML:** * `

` through `

` headings * `

` paragraphs * ``, ``, `` formatting * `