Skip to main content

Web Triggers API Reference for AI

This document is a complete API reference and code guide for writing scripts that run in Script Master's Web Triggers module β€” an Atlassian Forge app that lets users define backend JavaScript functions invoked by external HTTP requests, enabling integration between Jira/Confluence instances and third-party systems (webhooks, automation tools, CI/CD pipelines, etc.).

Use this document as context when asking an AI assistant (Claude, Gemini, ChatGPT, etc.) to generate Web Trigger scripts:

Here is the API reference: [link to this file]. Build a web trigger script for Script Master that [your task].


Execution Model​

Scripts run as async JavaScript functions executed on the Forge backend (Node.js 22.x, ARM64, 256 MB memory). This is a server-side FaaS environment β€” not a browser and not the user's machine. The Web Triggers module wraps your code like this:

const result = await (async function(api, route, fetch, authorize, request, console, /* ...JS globals */) {
// YOUR SCRIPT CODE HERE
})(api, route, fetch, authorize, request, wrappedConsole, /* ...JS globals */);

Key implications:

  • Top-level await is supported β€” use it freely.
  • The script must return a response object with at least a statusCode field. If the return value has no statusCode, Forge sends a 500 error to the caller.
  • console.log() / console.error() / etc. are captured and shown in the test panel. Maximum 100 log entries per run; each entry capped at 4 000 characters.
  • Errors thrown by the script are caught; the caller receives a 500 response.
  • Scripts run as the app (not as the current user) β€” use api.asApp() for all Atlassian API calls.
  • No browser APIs, no window, no DOM.
  • Web Trigger URLs are publicly accessible by default β€” there is no built-in authentication. You must implement authorization inside the script itself.

Available Globals​

Jira and Confluence (identical surface)​

GlobalTypeDescription
apiForgeAPIForge API β€” call api.asApp().requestJira(...) / api.asApp().requestConfluence(...)
routeTemplate-literal tagConstruct safe API route strings: route`/rest/api/3/issue/${id}`
fetch(url, options?) => Promise<Response>Standard fetch for external HTTP calls
authorizeForgeAuthorizeForge authorization utilities
requestWebTriggerRequestThe incoming HTTP request β€” method, headers, body, query params, context
consoleConsolelog, info, warn, error β€” output captured in the test panel

Standard JavaScript globals available: Array, Promise, String, Set, Object, Boolean, Symbol, BigInt, Number, Map, WeakMap, WeakSet, JSON, Intl, Math, RegExp, Error, Date, Buffer, ArrayBuffer, DataView, typed arrays, AbortController, AbortSignal, URL, URLSearchParams, TextEncoder, TextDecoder, crypto, atob, btob, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, setTimeout, setInterval, clearTimeout, clearInterval, parseFloat, parseInt, isFinite, isNaN.

Forbidden (will throw): eval, require, import(, process, new Function, new Proxy, structuredClone, queueMicrotask, escape, unescape, any access to .prototype, .__proto__, .constructor via user code, or arguments.callee.caller.


API Reference​

api.asApp().requestJira(route, options?)​

Makes an authenticated request to the Jira Cloud REST API as the installed app (service account level).

  • route β€” use the route template tag: route`/rest/api/3/...`
  • options β€” standard fetch RequestInit (method, headers, body)
  • Returns β€” a standard Response object
const response = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
headers: { 'Accept': 'application/json' },
});
const data = await response.json();
return {
body: JSON.stringify(data),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

api.asApp().requestConfluence(route, options?)​

Makes an authenticated request to the Confluence Cloud REST API as the installed app.

  • route β€” use the route template tag: route`/wiki/rest/api/...`
  • options β€” standard fetch options
  • Returns β€” a standard Response object
const response = await api.asApp().requestConfluence(route`/wiki/rest/api/user/current`, {
headers: { 'Accept': 'application/json' },
});
const data = await response.json();
return {
body: JSON.stringify(data),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

route (template literal tag)​

Constructs a safe API route string. Interpolated values are URL-encoded automatically.

const issueKey = 'PROJ-123';
const path = route`/rest/api/3/issue/${issueKey}`;
// β†’ "/rest/api/3/issue/PROJ-123"

Always use route instead of template literals when building Atlassian API paths.


fetch(url, options?)​

Standard fetch for calling external HTTP endpoints. No built-in authentication β€” pass your own credentials as needed.

const response = await fetch('https://api.example.com/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'issue_created' }),
});
return {
body: JSON.stringify({ forwarded: response.status }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

request (WebTriggerRequest)​

The incoming HTTP request object passed to every Web Trigger invocation.

interface WebTriggerRequest {
method: string; // "GET", "POST", "PUT", etc.
headers: Record<string, string[]>; // header names are lowercase
body: string; // raw request body as string
path: string; // path after the trigger base URL
queryParameters: Record<string, string[]>; // query params, values are arrays
context: {
cloudId: string; // Atlassian site cloud ID
moduleKey: string; // Forge module key of this trigger
userAccess?: {
enabled?: boolean;
};
};
}

Accessing request data:

// Parse JSON body
const payload = JSON.parse(request.body);

// Read a header (values are always arrays)
const authHeader = request.headers['authorization']?.[0];

// Read a query parameter
const projectKey = request.queryParameters['project']?.[0];

Response object (return value)​

Every script must return a WebTriggerResponse-shaped object. The statusCode field is required β€” omitting it causes Forge to respond with 500.

interface WebTriggerResponse {
statusCode: number; // REQUIRED
statusText?: string;
body?: string; // must be a string; JSON.stringify objects
headers?: Record<string, string[]>; // values must be arrays
}
// Minimal valid response
return { statusCode: 200 };

// Full response with JSON body
return {
body: JSON.stringify({ ok: true }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

// Error response
return { statusCode: 400, statusText: 'Bad Request' };

Code Patterns​

Pattern: Authorization via header check​

Web Trigger URLs are public by default. Always validate callers inside the script:

const SECRET = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
if (request.headers['authorization']?.[0] !== SECRET) {
return { statusCode: 403, statusText: 'Forbidden' };
}
// ... rest of handler

Pattern: Parse and validate JSON body​

let payload;
try {
payload = JSON.parse(request.body);
} catch {
return { statusCode: 400, statusText: 'Invalid JSON body' };
}

if (!payload.issueKey) {
return { statusCode: 422, statusText: 'Missing issueKey' };
}
// ... process payload

Pattern: Route by HTTP method​

if (request.method === 'GET') {
// handle GET
return { statusCode: 200, body: '...' };
}
if (request.method === 'POST') {
// handle POST
return { statusCode: 201, body: '...' };
}
return { statusCode: 405, statusText: 'Method Not Allowed' };

Pattern: Route by query parameter​

Consolidate multiple logical operations into one trigger using a query parameter:

const action = request.queryParameters['action']?.[0];

if (action === 'sync-issues') {
// ...
return { statusCode: 200, body: JSON.stringify({ synced: true }) };
}
if (action === 'status') {
return { statusCode: 200, body: JSON.stringify({ status: 'ok' }) };
}
return { statusCode: 400, statusText: `Unknown action: ${action}` };

Complete Examples​

Example 1: Echo the request (inspect all fields)​

return {
body: JSON.stringify({ request }, null, 2),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

Example 2: Authorization header check​

const YOUR_SECRET_KEY = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
if (request.headers['authorization']?.[0] !== YOUR_SECRET_KEY) {
return { statusCode: 403, statusText: 'Not Permitted' };
}
return { statusCode: 200, statusText: 'OK' };

Example 3: Jira REST API call (server info)​

const response = await api.asApp().requestJira(route`/rest/api/3/serverInfo`, {
headers: { 'Accept': 'application/json' },
});
const responseData = await response.json();
return {
body: JSON.stringify(responseData),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

Example 4: Jira response with custom headers and error branch​

if (typeof somethingWrong !== typeof undefined) {
return { statusCode: 400, statusText: 'Bad request' };
}
return {
body: JSON.stringify({ someKey: 'someValue' }),
headers: {
'Content-Type': ['application/json'],
'X-Request-Id': [`rnd-${Math.random()}`],
},
statusCode: 200,
statusText: 'OK',
};

Example 5: Jira license and user count​

const licenseResponse = await api.asApp().requestJira(route`/rest/api/3/instance/license`, {
headers: { 'Accept': 'application/json' },
});
const licenseData = await licenseResponse.json();

const userCountResponse = await api.asApp().requestJira(route`/rest/api/3/license/approximateLicenseCount`, {
headers: { 'Accept': 'application/json' },
});
const userCountData = await userCountResponse.json();

return {
body: JSON.stringify({ licenses: licenseData, users: userCountData }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

Example 6: Confluence REST API call​

const response = await api.asApp().requestConfluence(route`/wiki/rest/api/user/current`, {
headers: { 'Accept': 'application/json' },
});
const responseData = await response.json();
return {
body: JSON.stringify({ bodyContent: responseData }),
headers: { 'Content-Type': ['application/json'] },
statusCode: 200,
statusText: 'OK',
};

Example 7: Webhook receiver β€” create a Jira issue from an external event​

// Validate caller
const SECRET = 'your-shared-secret-here';
if (request.headers['x-webhook-secret']?.[0] !== SECRET) {
return { statusCode: 401, statusText: 'Unauthorized' };
}

// Parse payload
let event;
try {
event = JSON.parse(request.body);
} catch {
return { statusCode: 400, statusText: 'Invalid JSON' };
}

// Create Jira issue
const response = await api.asApp().requestJira(route`/rest/api/3/issue`, {
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({
fields: {
project: { key: 'OPS' },
issuetype: { name: 'Task' },
summary: `[External] ${event.title ?? 'Untitled event'}`,
description: {
type: 'doc',
version: 1,
content: [{ type: 'paragraph', content: [{ type: 'text', text: event.description ?? '' }] }],
},
},
}),
});

const issue = await response.json();
return {
body: JSON.stringify({ created: issue.key }),
headers: { 'Content-Type': ['application/json'] },
statusCode: response.status === 201 ? 201 : response.status,
statusText: response.statusText,
};

Writing Good Scripts β€” Rules​

  1. Always return an object with statusCode β€” any other return value (or no return) causes Forge to send a 500 to the caller. The statusCode field is not optional.
  2. Authenticate every request β€” Web Trigger URLs are publicly reachable. Always validate an Authorization header or a shared-secret query parameter before doing any work.
  3. JSON-stringify the body β€” the body field must be a string. Pass objects through JSON.stringify(...), not raw objects.
  4. Headers values must be arrays β€” headers is Record<string, string[]>. Write 'Content-Type': ['application/json'], not 'Content-Type': 'application/json'.
  5. Use api.asApp() β€” Web Triggers run as the Forge app, not as any Atlassian user. Always chain .asApp() before .requestJira() / .requestConfluence().
  6. Use the route tag for Atlassian paths β€” it auto-encodes interpolated values and is required for Forge's internal request routing.
  7. Parse body explicitly β€” request.body is always a plain string. Call JSON.parse(request.body) for JSON payloads; wrap in try/catch and return 400 on failure.
  8. Header and query param values are arrays β€” access them with ?.[0]: request.headers['content-type']?.[0], request.queryParameters['id']?.[0].
  9. Max 20 triggers per instance β€” for multiple logical operations, use one trigger and branch on a query parameter or HTTP method.
  10. No Node.js APIs β€” this is a Forge FaaS environment. fs, path, os, child_process, require, import( are all unavailable and will throw.

Jira Cloud REST API v3​

  • Server info: GET /rest/api/3/serverInfo β€” docs
  • Get current user: GET /rest/api/3/myself β€” docs
  • Search issues (JQL): GET /rest/api/3/search?jql=... β€” docs
  • Get issue: GET /rest/api/3/issue/{issueIdOrKey} β€” docs
  • Create issue: POST /rest/api/3/issue β€” docs
  • Update issue: PUT /rest/api/3/issue/{issueIdOrKey} β€” docs
  • Transition issue: POST /rest/api/3/issue/{issueIdOrKey}/transitions β€” docs
  • Add comment: POST /rest/api/3/issue/{issueIdOrKey}/comment β€” docs
  • Get projects: GET /rest/api/3/project/search β€” docs
  • Instance license: GET /rest/api/3/instance/license β€” docs
  • Approximate license count: GET /rest/api/3/license/approximateLicenseCount

Confluence REST API​

  • Get current user: GET /wiki/rest/api/user/current
  • Get spaces: GET /wiki/api/v2/spaces β€” docs
  • Get pages: GET /wiki/api/v2/pages β€” docs
  • Create page: POST /wiki/api/v2/pages β€” docs
  • Update page: PUT /wiki/api/v2/pages/{id} β€” docs
  • Delete/purge page: DELETE /wiki/api/v2/pages/{id}?purge=true β€” docs

Forge Web Triggers​

  • Forge Web Triggers overview: docs
  • Forge API (@forge/api): docs