Skip to content

Webhooks API

The Webhooks API provides endpoints for creating issues and deployments programmatically. This is useful for integrating GuideMode with custom tools, CI/CD pipelines, or systems that don’t have native integrations.

The v1 webhook API uses GuideMode’s internal data model directly, so no label mapping is required. All data created via these webhooks is tagged with provider "manual" to distinguish it from GitHub, Jira, or other native integrations.

https://app.guidemode.dev/api/webhooks/v1

All requests require an API key in the Authorization header:

Terminal window
curl -H "Authorization: Bearer gai_your_api_key" \
-H "Content-Type: application/json" \
https://app.guidemode.dev/api/webhooks/v1/issues

You can obtain an API key by:

  1. Running guidemode login in the CLI
  2. Creating one in the GuideMode web UI under Settings > API Keys
POST /api/webhooks/v1/issues

Creates a new issue or updates an existing one (matched by externalId).

{
"projectKey": "owner/repo",
"externalId": "JIRA-123",
"title": "Fix login button",
"type": "bug",
"state": "open",
"body": "The login button doesn't work on Safari",
"url": "https://jira.example.com/browse/JIRA-123",
"authorUsername": "developer1",
"assigneeUsername": "developer2",
"labels": ["critical", "ui"],
"createdAt": "2024-01-15T10:00:00Z",
"closedAt": null,
"metadata": {
"priority": "high",
"sprint": "Sprint 23"
}
}
FieldTypeRequiredDescription
projectKeystringYesProject identifier (e.g., "owner/repo")
externalIdstringYesUnique ID in your source system
titlestringYesIssue title
typestringYesOne of: feature, bug, chore, discovery, incident, other
statestringYesOne of: open, closed, in_progress
numbernumberNoIssue number (auto-generated if omitted)
bodystringNoIssue description
urlstringNoLink to source issue
authorUsernamestringNoCreator username
assigneeUsernamestringNoPrimary assignee
labelsstring[]NoLabel names
createdAtstringNoISO8601 timestamp (defaults to now)
closedAtstringNoISO8601 timestamp when closed
metadataobjectNoCustom key-value data
{
"success": true,
"action": "created",
"id": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "JIRA-123"
}

The action field indicates whether the issue was created or updated.

POST /api/webhooks/v1/deployments

Creates a new deployment or updates an existing one (matched by externalId).

{
"projectKey": "owner/repo",
"externalId": "deploy-123",
"ref": "main",
"sha": "abc123def456789012345678901234567890abcd",
"environment": "production",
"status": "success",
"task": "deploy",
"description": "Deploying v1.2.3",
"url": "https://myapp.example.com",
"creatorUsername": "deployer",
"isProduction": true,
"isRollback": false,
"rollbackFromSha": null,
"createdAt": "2024-01-15T10:00:00Z",
"metadata": {
"version": "1.2.3",
"buildNumber": "456"
}
}
FieldTypeRequiredDescription
projectKeystringYesProject identifier (e.g., "owner/repo")
externalIdstringYesUnique deployment ID
refstringYesBranch/tag being deployed
shastringYesCommit SHA (7-40 characters)
environmentstringYesOne of: production, staging, development, qa, preview, other
statusstringYesOne of: pending, queued, in_progress, success, failure, error, inactive
taskstringNoTask type (default: "deploy")
descriptionstringNoDeployment description
urlstringNoDeployment URL
creatorUsernamestringNoDeployer username
isProductionbooleanNoOverride production detection
isRollbackbooleanNoFlag as rollback (sets task to "rollback")
rollbackFromShastringNoSHA being rolled back from (7-40 characters)
createdAtstringNoISO8601 timestamp (defaults to now)
metadataobjectNoCustom key-value data
{
"success": true,
"action": "created",
"id": "550e8400-e29b-41d4-a716-446655440001",
"externalId": "deploy-123"
}
{
"error": "Validation failed",
"details": [
{ "path": "sha", "message": "sha must be 40 characters" },
{ "path": "type", "message": "Invalid enum value" }
]
}
{
"error": "Unauthorized"
}
{
"error": "Internal server error"
}

Create an issue:

Terminal window
curl -X POST https://app.guidemode.dev/api/webhooks/v1/issues \
-H "Authorization: Bearer gai_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"projectKey": "myorg/myrepo",
"externalId": "TICKET-123",
"title": "Implement dark mode",
"type": "feature",
"state": "open",
"labels": ["enhancement", "ui"]
}'

Record a deployment:

Terminal window
curl -X POST https://app.guidemode.dev/api/webhooks/v1/deployments \
-H "Authorization: Bearer gai_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"projectKey": "myorg/myrepo",
"externalId": "deploy-456",
"ref": "main",
"sha": "abc123def456789012345678901234567890abcd",
"environment": "production",
"status": "success"
}'
const API_KEY = process.env.GUIDEMODE_API_KEY;
const BASE_URL = 'https://app.guidemode.dev/api/webhooks/v1';
async function createIssue(issue: {
projectKey: string;
externalId: string;
title: string;
type: 'feature' | 'bug' | 'chore' | 'discovery' | 'incident' | 'other';
state: 'open' | 'closed' | 'in_progress';
body?: string;
labels?: string[];
}) {
const response = await fetch(`${BASE_URL}/issues`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(issue),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error: ${error.error}`);
}
return response.json();
}
async function recordDeployment(deployment: {
projectKey: string;
externalId: string;
ref: string;
sha: string;
environment: 'production' | 'staging' | 'development' | 'qa' | 'preview' | 'other';
status: 'pending' | 'queued' | 'in_progress' | 'success' | 'failure' | 'error';
}) {
const response = await fetch(`${BASE_URL}/deployments`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(deployment),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error: ${error.error}`);
}
return response.json();
}
import os
import requests
API_KEY = os.environ['GUIDEMODE_API_KEY']
BASE_URL = 'https://app.guidemode.dev/api/webhooks/v1'
def create_issue(project_key, external_id, title, issue_type, state, **kwargs):
response = requests.post(
f'{BASE_URL}/issues',
headers={
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
},
json={
'projectKey': project_key,
'externalId': external_id,
'title': title,
'type': issue_type,
'state': state,
**kwargs,
}
)
response.raise_for_status()
return response.json()
def record_deployment(project_key, external_id, ref, sha, environment, status, **kwargs):
response = requests.post(
f'{BASE_URL}/deployments',
headers={
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
},
json={
'projectKey': project_key,
'externalId': external_id,
'ref': ref,
'sha': sha,
'environment': environment,
'status': status,
**kwargs,
}
)
response.raise_for_status()
return response.json()

When you create an issue or deployment, if the specified projectKey doesn’t exist, GuideMode will automatically create a new project with:

  • provider: "manual"
  • sourceType: "manual"
  • syncIssues: true
  • syncPullRequests: true
  • syncDeployments: true

This means you can start tracking work items immediately without pre-configuring projects.

Both endpoints use upsert logic based on externalId:

  1. If an issue/deployment with the same tenantId + provider + externalId exists, it’s updated
  2. If no match is found, a new record is created

This makes it safe to call the API multiple times with the same externalId - you’ll always get the latest state synced.

  • 100 requests per minute per API key
  • 1000 requests per hour per API key

If you exceed these limits, you’ll receive a 429 Too Many Requests response.

  1. Use consistent external IDs: Use your source system’s unique identifiers as externalId to enable proper upsert behavior.

  2. Include timestamps: Provide createdAt and closedAt for accurate metrics. Without these, GuideMode uses the current time.

  3. Set appropriate types: Use the correct type for issues to enable proper categorization in analytics dashboards.

  4. Track deployment status changes: Call the deployment endpoint multiple times as status changes (pending → in_progress → success/failure).

  5. Use metadata for custom fields: Store additional data in the metadata field for reference, but note that it’s not indexed for queries.