Webhook acknowledgement (Linear-style)
Some providers (Linear, and similar) disable or retry a webhook if the receiver
answers with 202 Accepted. Aiden still starts the agent or app forward
asynchronously; you can configure the acknowledgement HTTP status and a few
response headers per webhook.
Use this when:
- The provider requires HTTP 200 (or 201/204) on the trigger URL
- You want a stable ack header such as
X-Provider-Hinton the HTTP response - An installed app target needs inbound signature headers (for example
Linear-Signature) on the forward path — create the webhook withresponse_status/response_headers, then trigger with those inbound headers
Allowed response_status values: 200, 201, 202, 204. Omit the field to
keep the platform default (202). Hop-by-hop names such as Authorization,
Set-Cookie, and Access-Control-* are rejected on create.
How to run
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
pip install stackgen-sdk
python examples/automation/webhook-ack-headers.py --agent my-oncall-agentexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
npm install stackgen-sdk tsx
npx tsx examples/automation/webhook-ack-headers.ts --agent my-oncall-agentexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
go run ./examples/go/automation/webhook-ack-headers.go --agent my-oncall-agentMinimal
from stackgen import StackgenClient, StackgenConfig
client = StackgenClient(
StackgenConfig(
base_url="https://app.stackgen.com",
api_token="stackgen_…",
workspace_id="<workspace-uuid>",
)
)
wh = client.aiden.create_webhook({
"name": "linear-issues",
"target_type": "agent",
"target_name": "my-oncall-agent",
"action": "Summarize the Linear issue and reply Done",
"enabled": True,
"response_status": 200,
"response_headers": {
"X-Ack-Source": "aiden",
"X-Provider-Hint": "linear-style",
},
})
print(wh["id"], wh.get("response_status"), wh.get("token"))import { StackgenClient, StackgenConfig } from "stackgen-sdk";
const client = new StackgenClient(
new StackgenConfig({
baseUrl: "https://app.stackgen.com",
apiToken: "stackgen_…",
workspaceId: "<workspace-uuid>",
}),
);
const wh = await client.aiden.createWebhook({
name: "linear-issues",
target_type: "agent",
target_name: "my-oncall-agent",
action: "Summarize the Linear issue and reply Done",
enabled: true,
response_status: 200,
response_headers: {
"X-Ack-Source": "aiden",
"X-Provider-Hint": "linear-style",
},
});
console.log(wh.id, wh.response_status, wh.token);wh, err := client.Aiden.CreateWebhook(ctx, map[string]any{
"name": "linear-issues",
"target_type": "agent",
"target_name": "my-oncall-agent",
"action": "Summarize the Linear issue and reply Done",
"enabled": true,
"response_status": 200,
"response_headers": map[string]any{
"X-Ack-Source": "aiden",
"X-Provider-Hint": "linear-style",
},
})
if err != nil { return err }
fmt.Println(wh["id"], wh["response_status"], wh["token"])
return nilTrigger and inspect the acknowledgement
The SDK’s high-level triggerWebhook helper returns the JSON body. To assert
the HTTP status and ack headers, call the trigger URL directly (as the
complete examples do):
curl -sS -D - -X POST \
"$STACKGEN_URL/guild/api/v1/webhooks/trigger?orgId=$STACKGEN_WORKSPACE_ID" \
-H "Authorization: Bearer $WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-H "Linear-Signature: test-sig" \
-d '{"action":"issue.create"}'
Expect HTTP/1.1 200 (or your configured status) and the configured
response_headers on the response. Processing continues in the background.
Complete scripts
- Python:
examples/automation/webhook-ack-headers.py - TypeScript:
examples/automation/webhook-ack-headers.ts - Go:
examples/go/automation/webhook-ack-headers.go
#!/usr/bin/env python3
"""Create a Linear-style webhook acknowledgement.
Providers such as Linear expect HTTP 200 (not 202) and may send signature
headers that apps need on forward. This sample:
1. Creates a webhook with response_status=200 and custom ack headers
2. Triggers it with a Linear-Signature header
3. Prints the acknowledgement status/headers (processing stays async)
Usage:
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
python examples/automation/webhook-ack-headers.py --agent my-oncall-agent
# Empty CI workspaces: prefer a published workflow (agents need an integration).
python examples/automation/webhook-ack-headers.py --workflow my-note-workflow
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
import uuid
from stackgen import StackgenClient, StackgenConfig
def require_env(name: str, *fallbacks: str) -> str:
for key in (name, *fallbacks):
value = (os.environ.get(key) or "").strip()
if value:
return value
raise SystemExit(f"missing env {name}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
target = parser.add_mutually_exclusive_group(required=True)
target.add_argument("--agent", help="Existing agent name")
target.add_argument("--workflow", help="Existing workflow name")
parser.add_argument(
"--message",
default="sdk: acknowledge Linear-style webhook and reply Done",
help="Webhook action prompt",
)
args = parser.parse_args()
target_type = "agent" if args.agent else "workflow"
target_name = args.agent or args.workflow
base_url = require_env("STACKGEN_URL").rstrip("/")
org_id = require_env("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT")
client = StackgenClient(
StackgenConfig(
base_url=base_url,
api_token=require_env("STACKGEN_TOKEN"),
workspace_id=org_id,
)
)
suffix = uuid.uuid4().hex[:8]
webhook_id = ""
try:
wh = client.aiden.create_webhook(
{
"name": f"sdk-ack-{suffix}",
"target_type": target_type,
"target_name": target_name,
"action": args.message,
"enabled": True,
# Providers that reject 202 can require 200.
"response_status": 200,
"response_headers": {
"X-Ack-Source": "stackgen-sdk",
"X-Provider-Hint": "linear-style",
},
}
)
webhook_id = str(wh.get("id") or "")
token = str(wh.get("token") or "")
print(f"created webhook id={webhook_id} response_status={wh.get('response_status')}")
print(f"response_headers={json.dumps(wh.get('response_headers') or {})}")
if not token:
raise SystemExit("createWebhook did not return a token")
# Trigger with a provider signature header. Ack is immediate; the agent
# run continues asynchronously.
url = f"{base_url}/guild/api/v1/webhooks/trigger?orgId={org_id}"
req = urllib.request.Request(
url,
data=json.dumps({"action": "issue.create", "source": "sdk-ack"}).encode("utf-8"),
method="POST",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"Linear-Signature": "sdk-demo-sig",
},
)
with urllib.request.urlopen(req, timeout=60) as resp:
body = resp.read().decode("utf-8")
print(f"trigger status={resp.status}")
print(f"trigger ack X-Ack-Source={resp.headers.get('X-Ack-Source')}")
print(f"trigger ack X-Provider-Hint={resp.headers.get('X-Provider-Hint')}")
print(f"trigger body={body}")
if resp.status != 200:
raise SystemExit(f"expected acknowledgement HTTP 200, got {resp.status}")
except urllib.error.HTTPError as err:
detail = err.read().decode("utf-8", errors="replace")
raise SystemExit(f"trigger failed status={err.code} body={detail}") from err
finally:
if webhook_id:
try:
client.aiden.delete_webhook(webhook_id)
print(f"deleted webhook id={webhook_id}")
except Exception as cleanup_err: # noqa: BLE001 — best-effort teardown
print(f"cleanup delete_webhook failed: {cleanup_err}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())/**
* Create a Linear-style webhook acknowledgement.
*
* Providers such as Linear expect HTTP 200 (not 202) and may send signature
* headers that apps need on forward. This sample:
*
* 1. Creates a webhook with response_status=200 and custom ack headers
* 2. Triggers it with a Linear-Signature header
* 3. Prints the acknowledgement status/headers (processing stays async)
*
* Usage:
* export STACKGEN_URL=https://app.stackgen.com
* export STACKGEN_TOKEN=stackgen_…
* export STACKGEN_WORKSPACE_ID=<workspace-uuid>
* npx tsx examples/automation/webhook-ack-headers.ts --agent my-oncall-agent
* # Empty CI workspaces: prefer a published workflow (agents need an integration).
* npx tsx examples/automation/webhook-ack-headers.ts --workflow my-note-workflow
*/
import { parseArgs } from "node:util";
import { randomUUID } from "node:crypto";
import { StackgenClient, StackgenConfig } from "stackgen-sdk";
function requireEnv(name: string, ...fallbacks: string[]): string {
for (const key of [name, ...fallbacks]) {
const value = (process.env[key] || "").trim();
if (value) {
return value;
}
}
throw new Error(`missing env ${name}`);
}
async function main(): Promise<void> {
const { values } = parseArgs({
options: {
agent: { type: "string" },
workflow: { type: "string" },
message: {
type: "string",
default: "sdk: acknowledge Linear-style webhook and reply Done",
},
},
});
if (Boolean(values.agent) === Boolean(values.workflow)) {
throw new Error("require exactly one of --agent or --workflow");
}
const targetType = values.agent ? "agent" : "workflow";
const targetName = values.agent || values.workflow!;
const baseUrl = requireEnv("STACKGEN_URL").replace(/\/$/, "");
const orgId = requireEnv("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT");
const client = new StackgenClient(
new StackgenConfig({
baseUrl,
apiToken: requireEnv("STACKGEN_TOKEN"),
workspaceId: orgId,
}),
);
const suffix = randomUUID().slice(0, 8);
let webhookId = "";
try {
const wh = await client.aiden.createWebhook({
name: `sdk-ack-${suffix}`,
target_type: targetType,
target_name: targetName,
action: values.message,
enabled: true,
// Providers that reject 202 can require 200.
response_status: 200,
response_headers: {
"X-Ack-Source": "stackgen-sdk",
"X-Provider-Hint": "linear-style",
},
});
webhookId = String(wh.id ?? "");
const token = String(wh.token ?? "");
console.log(
`created webhook id=${webhookId} response_status=${String(wh.response_status ?? "")}`,
);
console.log(`response_headers=${JSON.stringify(wh.response_headers ?? {})}`);
if (!token) {
throw new Error("createWebhook did not return a token");
}
const url = `${baseUrl}/guild/api/v1/webhooks/trigger?orgId=${encodeURIComponent(orgId)}`;
const resp = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"Linear-Signature": "sdk-demo-sig",
},
body: JSON.stringify({ action: "issue.create", source: "sdk-ack" }),
});
const body = await resp.text();
console.log(`trigger status=${resp.status}`);
console.log(`trigger ack X-Ack-Source=${resp.headers.get("X-Ack-Source")}`);
console.log(`trigger ack X-Provider-Hint=${resp.headers.get("X-Provider-Hint")}`);
console.log(`trigger body=${body}`);
if (resp.status !== 200) {
throw new Error(`expected acknowledgement HTTP 200, got ${resp.status}`);
}
} finally {
if (webhookId) {
try {
await client.aiden.deleteWebhook(webhookId);
console.log(`deleted webhook id=${webhookId}`);
} catch (err) {
console.warn("cleanup deleteWebhook failed:", err);
}
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});// Create a webhook that answers HTTP 200 with ack headers, then trigger it.
//
// Documented at https://appcd-dev.github.io/stackgen-sdk/code-samples/webhook-ack-headers/
//
// go run ./examples/go/automation/webhook-ack-headers.go --agent my-oncall-agent
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
stackgen "github.com/appcd-dev/stackgen-sdk/clients/go"
)
// envOr returns the first non-empty environment variable among keys.
func envOr(keys ...string) string {
for _, key := range keys {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
}
return ""
}
func main() {
agent := flag.String("agent", "", "Existing agent name")
workflow := flag.String("workflow", "", "Existing workflow name")
flag.Parse()
if (*agent == "") == (*workflow == "") {
fmt.Fprintln(os.Stderr, "pass exactly one of --agent or --workflow")
os.Exit(2)
}
base, token, org := envOr("STACKGEN_URL"), envOr("STACKGEN_TOKEN"), envOr("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT")
if base == "" || token == "" || org == "" {
fmt.Fprintln(os.Stderr, "set STACKGEN_URL, STACKGEN_TOKEN, and STACKGEN_WORKSPACE_ID")
os.Exit(2)
}
targetType, targetName := "agent", *agent
if *workflow != "" {
targetType, targetName = "workflow", *workflow
}
client, err := stackgen.NewClient(stackgen.Config{BaseURL: base, APIToken: token, WorkspaceID: org})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ctx := context.Background()
wh, err := client.Aiden.CreateWebhook(ctx, map[string]any{
"name": "linear-issues",
"target_type": targetType,
"target_name": targetName,
"action": "Summarize the Linear issue and reply Done",
"enabled": true,
"response_status": 200,
"response_headers": map[string]string{
"X-Ack-Source": "aiden",
"X-Provider-Hint": "linear-style",
},
})
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
webhookID, _ := wh["id"].(string)
webhookToken, _ := wh["token"].(string)
fmt.Printf("created webhook id=%s response_status=%v\n", webhookID, wh["response_status"])
defer func() {
if webhookID != "" {
_, _ = client.Aiden.DeleteWebhook(context.Background(), webhookID)
fmt.Printf("deleted webhook id=%s\n", webhookID)
}
}()
if webhookToken == "" {
fmt.Fprintln(os.Stderr, "create webhook did not return a token")
os.Exit(1)
}
body, _ := json.Marshal(map[string]string{"action": "issue.create", "source": "sdk-ack"})
endpoint := strings.TrimRight(base, "/") + "/guild/api/v1/webhooks/trigger?" + url.Values{"orgId": {org}}.Encode()
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
req.Header.Set("Authorization", "Bearer "+webhookToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Linear-Signature", "sdk-demo-sig")
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
fmt.Printf("trigger status=%d\n", resp.StatusCode)
fmt.Printf("trigger ack X-Ack-Source=%s\n", resp.Header.Get("X-Ack-Source"))
fmt.Printf("trigger ack X-Provider-Hint=%s\n", resp.Header.Get("X-Provider-Hint"))
fmt.Printf("trigger body=%s\n", raw)
if resp.StatusCode != 200 {
os.Exit(1)
}
}