Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.devtune.ai/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks let you receive real-time HTTP notifications when events occur in your project. DevTune will POST a JSON payload to your configured URL whenever a subscribed event fires.

Event Types

EventDescription
search-tracking.completedA search tracking run has finished processing
visibility.changedVisibility metrics have been calculated after a search tracking run
action.createdAn action has been created by DevTune, a user, or the API
action.updatedA customer-visible action field changed
action.recommendation.createdA new visible recommendation appears in the Actions workspace
action.recommendation.updatedA visible recommendation changes materially and should be reviewed again
action.* events track the action entity lifecycle. action.recommendation.* events track the recommendation surface. Recommendation update events are intentionally quiet: they do not fire for recommendation reorder, accepted/backlog movement, small citation drift, evidence reorder, wording-only changes, or brief absence alone.

Managing Webhooks via the Dashboard

You can create and manage webhooks directly from your account settings in the DevTune dashboard.
  1. Navigate to your team account
  2. Open Webhooks in the account sidebar
  3. Click Create Webhook
  4. Fill in the form:
    • Endpoint URL — The HTTPS URL that will receive webhook POST requests
    • Project — Select which project’s events should trigger the webhook
    • Events — Check one or more event types to subscribe to
  5. Click Create Webhook
  6. Copy the signing secret shown in the dialog — this is the only time it will be displayed
  7. If you ever need a new secret, click the rotate icon next to the webhook — this generates a new secret and invalidates the old one immediately
You can view all your webhooks in the table, and delete any active webhook using the trash icon. Use the “Hide inactive” toggle to filter out previously deleted webhooks.
Note: The Webhooks page requires the Plus plan or above (API Access entitlement). You must also have the settings.manage permission on the team account.

Managing Webhooks via the API

Scoped API keys must include:
  • webhooks.read to list webhook subscriptions
  • webhooks.write to create, delete, or rotate webhook secrets

Create a Webhook

POST /api/v2/projects/{projectId}/webhooks

Request Body

{
  "url": "https://your-app.com/webhooks/devtune",
  "events": ["action.created", "action.recommendation.created"]
}

Response (201)

{
  "data": {
    "id": "uuid",
    "url": "https://your-app.com/webhooks/devtune",
    "events": ["action.created", "action.recommendation.created"],
    "isActive": true,
    "secret": "a1b2c3d4...hex-string",
    "createdAt": "2026-02-08T12:00:00.000Z"
  },
  "meta": { "timestamp": "...", "projectId": "..." }
}
The secret is only returned when creating the webhook. Store it securely for signature verification.

List Webhooks

GET /api/v2/projects/{projectId}/webhooks
Returns all active webhook subscriptions for the project.

Response (200)

{
  "data": {
    "webhooks": [
      {
        "id": "uuid",
        "url": "https://your-app.com/webhooks/devtune",
        "events": ["action.created", "action.recommendation.created"],
        "isActive": true,
        "createdAt": "2026-02-08T12:00:00.000Z"
      }
    ]
  },
  "meta": { "timestamp": "...", "projectId": "..." }
}

Delete a Webhook

DELETE /api/v2/projects/{projectId}/webhooks/{webhookId}
Deactivates the webhook subscription.

Response (200)

{
  "deleted": true
}
Returns 404 if the webhook ID does not exist or belongs to a different project.

Rotate Webhook Secret

POST /api/v2/projects/{projectId}/webhooks/{webhookId}/rotate-secret
Generates a new HMAC-SHA256 signing secret for the webhook. The old secret is invalidated immediately — any deliveries signed with the old secret will no longer verify. No request body is required.

Response (200)

{
  "data": {
    "secret": "new-a1b2c3d4...hex-string"
  },
  "meta": { "timestamp": "...", "projectId": "..." }
}
Returns 404 if the webhook does not exist, belongs to a different project, or is inactive.

Webhook Payload Format

When an event fires, DevTune sends a POST request with this body:
{
  "event": "search-tracking.completed",
  "projectId": "your-project-id",
  "accountId": "your-account-id",
  "timestamp": "2026-02-08T12:00:00.000Z",
  "data": {
    "runId": "run-uuid",
    "finalStatus": "completed"
  }
}

visibility.changed payload

{
  "event": "visibility.changed",
  "projectId": "your-project-id",
  "accountId": "your-account-id",
  "timestamp": "2026-02-10T06:00:00.000Z",
  "data": {
    "runId": "run-uuid",
    "capturedOn": "2026-02-10",
    "metrics": {
      "visibilityScore": 0.75,
      "citationCount": 12,
      "brandMentionCount": 5,
      "sampleCount": 20
    },
    "previousMetrics": {
      "visibilityScore": 0.6,
      "citationCount": 8,
      "brandMentionCount": 3,
      "sampleCount": 18,
      "capturedOn": "2026-02-09"
    }
  }
}
previousMetrics is null when there is no prior data (e.g., the first run for a project).

action.created payload

{
  "event": "action.created",
  "projectId": "your-project-id",
  "accountId": "your-account-id",
  "timestamp": "2026-05-02T12:00:00.000Z",
  "data": {
    "event": "action.created",
    "actionId": "action-uuid",
    "origin": "user",
    "actor": { "type": "user", "userId": "user-uuid" },
    "changedFields": [],
    "action": {
      "id": "action-uuid",
      "title": "Update the pricing page integration section",
      "description": "Competitors are being cited for integration details that your page does not cover.",
      "status": "backlog",
      "surface": "backlog",
      "priority": "high",
      "channels": ["content", "product"],
      "target": { "label": "/pricing", "type": "page", "payload": {} },
      "expectedImpact": "Improve coverage for high-intent pricing prompts.",
      "whatChanged": "Competitor pages gained citations for pricing integration questions.",
      "updatedAt": "2026-05-02T11:58:10.000Z"
    },
    "links": {
      "apiActionContext": "/api/v2/projects/your-project-id/actions/list?detailLevel=context",
      "mcpActionContext": {
        "tool": "devtune_get_actions",
        "args": { "detailLevel": "context" }
      },
      "mcpActionBrief": {
        "tool": "devtune_get_action_brief",
        "args": { "actionId": "action-uuid" }
      }
    }
  }
}

action.updated payload

{
  "event": "action.updated",
  "projectId": "your-project-id",
  "accountId": "your-account-id",
  "timestamp": "2026-05-02T12:00:00.000Z",
  "data": {
    "event": "action.updated",
    "actionId": "action-uuid",
    "origin": "user",
    "actor": { "type": "user", "userId": "user-uuid" },
    "changedFields": ["status", "assigned_to"],
    "action": {
      "id": "action-uuid",
      "title": "Update the pricing page integration section",
      "status": "in_progress",
      "surface": "backlog",
      "priority": "high"
    },
    "links": {
      "apiActionContext": "/api/v2/projects/your-project-id/actions/list?detailLevel=context",
      "mcpActionContext": {
        "tool": "devtune_get_actions",
        "args": { "detailLevel": "context" }
      }
    }
  }
}

action.recommendation.created payload

{
  "event": "action.recommendation.created",
  "projectId": "your-project-id",
  "accountId": "your-account-id",
  "timestamp": "2026-05-02T12:00:00.000Z",
  "data": {
    "event": "action.recommendation.created",
    "actionId": "action-uuid",
    "origin": "system",
    "actor": { "type": "system" },
    "changedFields": [],
    "action": {
      "id": "action-uuid",
      "title": "Update the pricing page integration section",
      "status": "active",
      "surface": "recommendations",
      "priority": "high",
      "channels": ["content", "product"],
      "target": { "label": "/pricing", "type": "page", "payload": {} },
      "expectedImpact": "Improve coverage for high-intent pricing prompts.",
      "whatChanged": "Competitor pages gained citations for pricing integration questions.",
      "updatedAt": "2026-05-02T11:58:10.000Z"
    },
    "recommendationContext": {
      "bucket": "do_now",
      "scores": { "opportunity": 0.84, "effort": 0.35 },
      "whyNow": "Recent prompt evidence shows the gap is active.",
      "topEvidence": [
        {
          "id": "evidence-block-uuid",
          "title": "Prompt landscape on pricing",
          "summary": "12 tracked prompts generated 88 citations in the last 30 days.",
          "type": "citation_shift"
        }
      ],
      "briefReadiness": {
        "hasBrief": false,
        "status": null,
        "generatedAt": null,
        "updatedAt": null
      },
      "changeReasons": []
    },
    "links": {
      "apiActionContext": "/api/v2/projects/your-project-id/actions/list?detailLevel=context",
      "mcpActionContext": {
        "tool": "devtune_get_actions",
        "args": { "detailLevel": "context" }
      },
      "mcpActionBrief": {
        "tool": "devtune_get_action_brief",
        "args": { "actionId": "action-uuid" }
      }
    },
    "review": {
      "classification": "brief_stale_only",
      "fingerprintKey": "stable-review-fingerprint"
    }
  }
}

action.recommendation.updated payload

action.recommendation.updated uses the same payload shape as action.recommendation.created, with recommendationContext.changeReasons explaining why review is recommended.
{
  "event": "action.recommendation.updated",
  "projectId": "your-project-id",
  "accountId": "your-account-id",
  "timestamp": "2026-05-02T12:00:00.000Z",
  "data": {
    "event": "action.recommendation.updated",
    "actionId": "action-uuid",
    "origin": "system",
    "actor": { "type": "system" },
    "changedFields": ["target_label", "priority"],
    "action": {
      "id": "action-uuid",
      "title": "Update the pricing page integration section",
      "status": "active",
      "surface": "recommendations",
      "priority": "high"
    },
    "recommendationContext": {
      "bucket": "do_now",
      "changeReasons": ["concrete_target_changed", "prompt_set_changed"]
    },
    "links": {
      "apiActionContext": "/api/v2/projects/your-project-id/actions/list?detailLevel=context",
      "mcpActionContext": {
        "tool": "devtune_get_actions",
        "args": { "detailLevel": "context" }
      }
    }
  }
}
To fetch full context after either event, use:
  • GET /api/v2/projects/{projectId}/actions/list?detailLevel=context
  • GET /api/v2/projects/{projectId}/actions/{actionId}/brief
  • MCP devtune_get_actions with detailLevel: "context"
  • MCP devtune_get_action_brief

Headers

HeaderDescription
Content-Typeapplication/json
X-DevTune-SignatureHMAC-SHA256 hex digest of the request body
X-DevTune-EventThe event type (e.g., search-tracking.completed)
User-AgentDevTune-Webhook/2.0

Verifying Signatures

Every webhook delivery includes an X-DevTune-Signature header containing an HMAC-SHA256 hex digest computed with your webhook secret.

Node.js Example

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  const received = Buffer.from(signature, 'hex');
  if (expected.length !== received.length) return false;
  return crypto.timingSafeEqual(received, expected);
}
Important: Use the raw request body (before JSON parsing) for signature verification. In Express, use express.raw({ type: 'application/json' }) on your webhook route to get the raw buffer.

Python Example

import hmac
import hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Retry Policy

Failed deliveries (non-2xx responses or timeouts) are retried up to 3 times with exponential backoff. Each attempt is logged in the delivery log visible in the DevTune dashboard.

Use Cases

  • Slack notifications when a tracking run finishes
  • CI/CD triggers to re-run checks when visibility changes
  • Data pipelines that sync DevTune data to your warehouse on each update
  • GTM automation that feeds generated actions into triage systems or agentic workflows