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: https://docs.apportunity.xyz/script-master/code-editor/webtrigger-ai-prompt. 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
awaitis supported - use it freely. - The script must
returna response object with at least astatusCodefield. If the return value has nostatusCode, Forge sends a500error 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
500response. - 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)β
| Global | Type | Description |
|---|---|---|
api | ForgeAPI | Forge API - call api.asApp().requestJira(...) / api.asApp().requestConfluence(...) |
route | Template-literal tag | Construct safe API route strings: route`/rest/api/3/issue/${id}` |
fetch | (url, options?) => Promise<Response> | Standard fetch for external HTTP calls |
authorize | ForgeAuthorize | Forge authorization utilities |
request | WebTriggerRequest | The incoming HTTP request - method, headers, body, query params, context |
console | Console | log, 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 theroutetemplate tag:route`/rest/api/3/...`options- standardfetchRequestInit(method, headers, body)- Returns - a standard
Responseobject
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 theroutetemplate tag:route`/wiki/rest/api/...`options- standardfetchoptions- Returns - a standard
Responseobject
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β
- Always return an object with
statusCode- any other return value (or no return) causes Forge to send a500to the caller. ThestatusCodefield is not optional. - Authenticate every request - Web Trigger URLs are publicly reachable. Always validate an
Authorizationheader or a shared-secret query parameter before doing any work. - JSON-stringify the body - the
bodyfield must be astring. Pass objects throughJSON.stringify(...), not raw objects. - Headers values must be arrays -
headersisRecord<string, string[]>. Write'Content-Type': ['application/json'], not'Content-Type': 'application/json'. - Use
api.asApp()- Web Triggers run as the Forge app, not as any Atlassian user. Always chain.asApp()before.requestJira()/.requestConfluence(). - Use the
routetag for Atlassian paths - it auto-encodes interpolated values and is required for Forge's internal request routing. - Parse
bodyexplicitly -request.bodyis always a plain string. CallJSON.parse(request.body)for JSON payloads; wrap in try/catch and return400on failure. - Header and query param values are arrays - access them with
?.[0]:request.headers['content-type']?.[0],request.queryParameters['id']?.[0]. - Max 20 triggers per instance - for multiple logical operations, use one trigger and branch on a query parameter or HTTP method.
- No Node.js APIs - this is a Forge FaaS environment.
fs,path,os,child_process,require,import(are all unavailable and will throw.
Useful REST API Linksβ
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