Submit an approval decision through the Web API

Description

Why

Customers who run part of a process outside Jira or Confluence have to walk a person through a browser to record a decision. The Web API can create, delete and archive approvals and mint signed action links, but every such link needs a login, which ends the road for an unattended process.

One operation is missing: record a decision with a comment on a given approval, on behalf of a given approver. The planned conversational agent needs the same decision rules, so the rules are built once, in the decision engine and one shared check, and serve every channel. The agent will add its own audit source.

A decision submitted by an integration must be told apart from one the person took. That only works if every decision records the channel it came through. Marking only the Web API would make "unmarked" an implicit claim that the person clicked, which is not true for e-mail links, Slack or the approver portal either.

What the user can do

Turning it on

  • A global administrator turns on "Approval decisions through the Web API" in global settings (General, section "Web API"). It is off by default. The setting states the risk in the interface: whoever holds an API key with the decision privilege can record a decision in an approver's name, and the app cannot tell whether that person really decided. Every such decision is marked in the approval.

  • Whoever edits a definition turns on "Allow decisions through the Web API" on it. The option is marked "New".

    • While the global setting is off, the option is disabled; its tooltip says it has to be enabled in global settings first. The value saved on the definition stays, so turning the global setting back on restores it.

    • The Web API cannot change the option: definition updates through the Web API keep its stored value.

    • The option is copied into each approval when it starts, so turning it on or off applies to approvals started afterwards. Turning the global setting off stops Web API decisions on all approvals immediately, including running ones.

The API key

  • A global administrator grants an API key the "Submit approval decisions" privilege. It is the Update cell of the Approval row in the privilege matrix, with a tooltip explaining it, and "Select all" includes it. While the global setting is off, the cell is disabled and "Select all" skips it. The key decides as:

    • its owner, when it has no performer origin;

    • the fixed user chosen on the key (performer origin "User"). This is the recommended mode: the key can only ever decide as that one person, for example a service account that is the approver of the integration's step;

    • the person named in each call (performer origin "User from API"). That person must belong to the group chosen on the key. This is the mode that carries the risk.

    • the person in the work item field, the page creator or the page last modifier, read from the approval's work item or page at each call (performer origins "Work item field User", "Page creator", "Page modifier"). Whoever can edit that field or page steers whose name the decision is recorded under; the customer opts in to that risk, and every decision is audited.

  • The key's "default user" is never used for decisions. When the field is empty or the page names no creator or last modifier, the call is refused (409, PERFORMER_NOT_RESOLVED).

The call

  • The external system calls one operation, POST /webapi/v2/approvals/{approvalId}/decisions, with:

    • the decision (approve, reject, abstain or consent) and a comment;

    • the person (userId), only for "User from API" keys;

    • optionally the step;

    • optionally onBehalfOf, the approver whose slot a delegate fills;

    • a request id.

    No browser, no login, no action link.

  • The app applies the same rules as the approval panel:

    • the person is an approver on an active user or group step, directly or through an active delegation, is not excluded from the group step (nor is the delegator), and has not decided on it yet;

    • the decision fits the step: approve only on approval steps, consent only on consent steps, reject always, abstain only when the approval and the global settings allow it;

    • a comment is present when comments are required for that decision both in global settings and on the step;

    • the approval is not archived, completed, incomplete, past its decision deadline or outdated (Confluence page changed).

  • The person must have real access to the work item or page. They can browse it, with issue security and page restrictions included. When the app's work item or page permissions are enabled, the person must also pass the same "view approvals" rule as the panel. The check runs with the app's credentials, so it works for people who never opened the app. Inactive accounts and JSM portal customers are refused.

Seeing where a decision came from

  • Every decision records its channel: panel, e-mail link, action link (a link minted through the Web API step links), Slack, approver portal or Web API.

  • On the approval panel, a decision taken outside the panel shows its channel next to the decision date ("via Slack", "via Web API", ...). This applies to user and e-mail step decisions, each group and vote entry, and the vote moderator's decision.

  • Activity shows the same marker for user and e-mail step decisions. Activity has no per-member rows for group and vote steps; those are marked on the step in the panel.

  • Panel decisions (single and bulk, including the JSM portal, the dashboard gadget and the global approvals page) stay unmarked. Decisions recorded before this change carry no marking.

  • A Web API decision's details show:

    • to anyone who can see the approval: the API key name as it was at the call, and the decision time;

    • to a global administrator, in addition: the key fingerprint, the key owner, the source IP, the request id and a link to the key's page (usage is kept for 7 days). The link also works for a key that has since been archived.

  • The Activity CSV export has a "Channel" column, and the step decision webhook carries channel.

Fixes included

These are defects in the same area. The new operation depends on them, or they would let the panel and the other channels break the rules the Web API enforces.

  1. The client IP can be spoofed. The app takes the client address from forwarded headers without trusting only the proxies (server.forward-headers-strategy=framework). A client-sent RFC 7239 Forwarded: for= header wins, so any caller can pass any API key's IP allowlist and write any IP into the usage log. After the fix, only the app's own proxies are trusted, and Forwarded is ignored.

  2. Excluded group members can decide. The decision engine does not enforce "excluded" group members (for example "exclude approval creator"). A crafted panel or bulk request lets an excluded user decide on a group step whose membership is resolved live. A delegate of an excluded member can also vote. After the fix, the engine refuses both.

  3. Archived delegations still work. The engine accepts an archived (revoked) delegation when the client names it. After the fix, it refuses it like an out-of-range one.

  4. A delegated decision can turn into a direct vote or get lost.

    • A delegated group or vote-step decision becomes the person's own vote when the delegation stopped applying in the meantime (for example the delegator voted).

    • A delegate's decision on someone else's user step, or in the vote moderator's place, is dropped when its delegation no longer resolves, while the panel or the link reports success. This happens for example with an e-mail link sent before the delegator edited the delegation.

    After the fix, a decision on a named delegation is recorded for the delegator's slot or refused with an error, never silently dropped. The panel's "myself" option and decisions on a person's own user step or as a vote moderator are unaffected.

  5. Mismatched decisions corrupt steps. A "vote" sent to a user, e-mail or group step rejects the step or silently uses up the user's vote. This is possible through bulk decide, or the panel while a vote step is active. After the fix, the engine refuses a decision that does not fit the step.

  6. Required comments are enforced differently per channel:

    • not for delegated user-step decisions in the panel;

    • not at all in the approver portal;

    • not after submit in Slack;

    • bypassable in the panel with withComment=false;

    • whitespace counts as a comment in some channels.

    After the fix, the engine enforces the step-specific rule for every channel, and every screen that marks a comment as required uses the same rule.

  7. Voting options are checked against the wrong vote step. The check uses the first active vote step, not the one being voted on. After the fix, it uses the targeted step.

  8. Bulk decide skips the "view approvals" app permission that the single decision checks. After the fix, it applies it.

  9. App permission failures open everything. When reading the app's permission settings fails, the app treats work item and page permissions as disabled. That briefly opens every permission-gated feature to every user. After the fix, a failed read denies instead, and the settings are cached briefly, which also saves a call on every app load.

  10. Web API calls with a named performer return HTTP 500 ("App update required") for a performer who never opened the app. After the fix, the performer's access is checked with the app's credentials, so the call answers properly. A Confluence listing that spans several pages is checked in one call as the performer and falls back to the app's credentials when the performer has no token. Creating a Confluence approval is the exception; see "Out of scope".

  11. API key settings are under-validated. Keys can be saved with "User from API" and no group, a performer origin switched on but not chosen, "Work item field User" with no field, or a performer origin the product does not support. After the fix, the key form and the backend refuse these.

  12. Confluence uses a group id as a user id. When a page performer cannot be resolved, the Confluence Web API falls back to the key's group id as if it were a user id. After the fix, it uses the key's default user, as Jira does. A key that holds the performer privilege without an origin (possible for keys saved before fix 11) answered HTTP 500 on Confluence and 400 on Jira; after the fix both answer 400.

  13. examples.http contains bearer tokens for the production and qa hosts. The file now uses a placeholder. The tokens stay in git history, so the keys behind them must be archived.

  14. Decision details are lost when an approval is saved again.

    • A copied vote step (a new approval version after a reassignment or a dynamic field update, and the view shown to JSM customers) loses the moderator's decision.

    • Re-saving a group step clears the delegation reference of entries recorded by delegates.

    After the fix, both are kept.

  15. E-mail and action links hide why a decision was refused. When the decision engine refuses a decision from an e-mail link or an action link (for example a delegation that no longer applies or was archived), the page shows only "An error occurred while executing the action". The panel, Slack and the approver portal show the reason. After the fix, the links show it too. The error page shows such messages as plain text, because they can contain step names set by definition editors. Its lost-permissions message therefore no longer contains HTML.

Acceptance criteria

Settings and configuration

  • Global settings hold "Approval decisions through the Web API", off by default, on the General page, with the risk stated next to the toggle. Only a global administrator can see and change it.

  • The definition editor shows "Allow decisions through the Web API" with a "New" badge. While the global setting is off it is disabled, and its tooltip explains that it must be enabled in global settings. No save clears its stored value, including a save while the global setting is off.

  • Everyone who can edit the definition can change the option. Duplicating a definition turns the option off in the copy. A Web API definition update keeps the stored value; a Web API definition create turns it off.

  • An approval accepts Web API decisions only if the global setting is on and the option was on when the approval started. Create-approval parameters cannot turn it on.

  • "Submit approval decisions" is the Update cell of the Approval row in the API key privilege matrix, off by default, with a tooltip. "Select all" and the row and column toggles include it. While the global setting is off it cannot be newly granted (UI and backend), and the toggles skip it. A key that already holds it keeps it and can have it removed.

  • A key holding the privilege decides as its owner (no performer origin), as the user chosen on the key ("User"), as the person named in the call ("User from API" with a group), or as the person in the work item field, the page creator or the page last modifier, read at each call. The key's default user is never used.

  • A key holding the privilege with an unrestricted IP range (any /0 entry) is flagged on the key list, and the form warns about it before saving. The key's configuration history shows the privilege.

The operation

  • POST /webapi/v2/approvals/{approvalId}/decisions records the decision through the same engine as the panel and returns the approval id, step id, step name, decision, person, onBehalfOf, decision time, step status, approval status, request id and replayed.

  • A refused call records no decision and no audit row, and its response carries a machine-readable reason and a self-contained message naming the offending value. It is refused when:

    • the global setting is off (403);

    • the approval is unknown on this site, or the key owner can no longer see it (404, the same response in both cases);

    • the approval's definition does not allow it, as defined above (403);

    • the key lacks the privilege (403, existing "Unset privilege");

    • userId is sent to a key that does not use "User from API" and differs from the person the key decides as (its owner, its user, or the person read from the work item or page), is missing for a "User from API" key, or does not exist (400);

    • "Allow change performer origin" is on without an origin, or the origin lacks its user, group or field (400,PERFORMER_NOT_SUPPORTED);

    • the work item field is empty, or the page names no creator or last modifier (409, PERFORMER_NOT_RESOLVED);

    • the person is inactive, is a JSM portal customer, is not in the key's group, or has no real access to the work item or page (403). Group and access refusals are also recorded in the instance error feed with the key, the person, the approval and the source IP;

    • the approval is archived, completed, incomplete, past its deadline or outdated (409);

    • the person has no active step they can decide on, or the given step does not accept decisions, is not theirs, is already decided by them, or they (or the approver they act for) are excluded from it (409);

    • the given step is a vote step or an e-mail step (400, "not supported through the Web API");

    • the decision does not fit the step (400);

    • a required comment is missing, or the comment is longer than 1000 characters (400);

    • the request id, userId, onBehalfOf or step id is malformed (400);

    • Atlassian cannot answer the access check (503; nothing is recorded, retry is safe).

  • With exactly one decidable slot (each delegated slot counts separately) the step is resolved automatically. With more, nothing is recorded and the response lists the candidates (stepId, step name, onBehalfOf). Never a first match.

  • A repeated call with the same request id on the same key returns the original decision with replayed: true and records nothing new. This holds when the calls run concurrently, and also after the global setting or the definition option was turned off. Fields that the first call omitted may be added on the retry (for example the stepId from the first response). A different approval, decision, comment, person, step or onBehalfOf under the same request id is refused (409).

  • Turning the global setting off takes effect on the next call. Turning the definition option off affects only approvals started afterwards.

Channel and audit

  • Each channel stores its channel on the decision:

    • panel and bulk decide → Panel;

    • no-login e-mail action link → Email link;

    • login-required link minted through the Web API step links → Action link;

    • Slack → Slack;

    • external approver portal → Approver portal;

    • Web API → Web API.

    The channel survives a new approval version (reassignment, dynamic field update).

  • The panel marks non-panel decisions on user and e-mail steps, group entries, vote entries and moderator decisions. Activity marks non-panel user and e-mail step decisions. Panel decisions and older decisions are unmarked.

  • Web API decision details are shown by role as described. JSM portal customers and external approvers see no API key details, and anonymized vote entries carry no channel.

  • The Activity CSV has a "Channel" column. It is filled for every user and e-mail step decision recorded after this change, panel included, and empty for older ones.

  • The step decision webhook carries channel for every decision, panel included: panel, email-link, action-link, slack, approver-portal, web-api, and system for steps resolved automatically.

  • The Web API reference (OpenAPI) documents the operation, its request and response, every status and reason, and the idempotency rules. The documentation page covers the operation, both settings, the privilege, the performer modes and the security guidance.

Fixes

  • Each of the fixes 1–12, 14 and 15 above behaves as described, in every channel it names.

Constraints

  • An API key is a secret of the customer's system. "X approved" through the Web API is that system's attestation, not proof that X clicked. Hence:

    • the double opt-in (the global setting, set by a global administrator, and the definition option, set by whoever edits the definition);

    • the privilege, which decides as the key owner or an explicit performer or, only when the customer opts in with a document performer origin, as the person the work item field or page names; never as the key's default user;

    • the visible channel in the approval;

    • the risk stated in the interface.

  • "User from API" and the document performer origins are where the risk sits. The group on the key (for "User from API"), the person's real access, the IP allowlist, the instance error feed and the decision audit narrow and expose it; none of them is presented as making it safe.

  • Which steps the key owner or the fixed user decides on is still shaped by the definition's editors and by work item fields that route steps to users. The documentation says so.

  • Pages that list approvals or activity make no extra Atlassian API calls. API key details are loaded only when someone opens them.

Out of scope

  • Vote steps (voting and the moderator's decision). Votes carry voting options and can be anonymized for JSM customers, which needs its own design for the "via Web API" details. Refused with a clear message.

  • E-mail steps. Their approver has no Atlassian account, so neither the key's group nor real access can be checked.

  • JSM portal customers as the named person.

  • Marking the channel in notification texts (watcher e-mails, Slack thread updates, status comments).

  • Rate limiting of the Web API.

  • Restricting a key to particular definitions or projects.

  • Creating a Confluence approval through the Web API for a performer who never opened the app. The engine reads the page as the approval's creator, so this call still answers "App update required" until the performer opens the app once.

Manual tester notes

Run these on qa after deployment, in addition to the acceptance criteria. Automated tests cannot cover them: they depend on the real proxy chain, real Atlassian permissions or real timing.

Client IP and proxies (fix 1)

  • Call any Web API endpoint with -H 'Forwarded: for=203.0.113.9'. The key's Usage tab must show your real IP, not203.0.113.9.

  • Repeat with -H 'X-Forwarded-For: 203.0.113.9'; the real IP must be shown again.

  • A key whose allowed IPs contain only your real IP works. A key whose allowed IPs contain only 203.0.113.9 is refused, even when the request sends that address in Forwarded or X-Forwarded-For.

  • Everything that builds absolute URLs or redirects still works over https on the right host:

    • logging in to the external approver portal, and a step-action link that requires login (redirect to login and back);

    • e-mail action links without login;

    • Slack buttons;

    • Microsoft e-mail OAuth authorization in settings;

    • the app loading in Jira and Confluence;

    • the error JSON of a failing Web API call (its url must start with the public https host).

Web API decisions

  • With a "User" key whose fixed user never opened the app, a decision succeeds. There must be no 500.

  • With a key without a performer origin, a decision is recorded as the key owner; sending another person's userId is refused with USER_ID_NOT_ACCEPTED.

  • With a "Work item field User" key, a decision is recorded as the user in the field; with the field empty, the call answers 409 PERFORMER_NOT_RESOLVED and the default user is not used. In Confluence, the same with "Page creator" and "Page modifier".

  • A person who cannot browse the work item (issue security level, or project permissions) or the page (page restriction) is refused, and the instance error feed shows the attempt. A second attempt for the same person from another IP, or on another approval, shows as a separate entry with its own IP and approval.

  • With work item or page permissions enabled in the app, a person without the "view approvals" permission is refused. With them disabled, the same person is accepted.

  • A JSM portal customer named as the person is refused.

  • Two identical calls sent at the same time (same request id) produce one decision; the other response says replayed: true.

  • A delegated call (onBehalfOf) for a delegator who has already voted on the group step is refused. It must not record a vote for the person.

  • A group step that needs two approvals: one Web API decision and one panel decision complete it; the panel shows which entry came through the Web API.

  • Turn the global setting off while an approval is running: the next call is refused, and repeating an earlier request id still returns its result.

  • Turn the definition option off: calls on running approvals of that definition still succeed; an approval started afterwards is refused.

  • A project administrator (not a global administrator) edits an opted-in definition: they can turn the option off and on, and the save keeps their choice.

Display

  • Open the Web API decision details:

    • as a global administrator: key name, fingerprint, owner, IP, request id, and the link opens the key on its Usage tab, also for an archived key;

    • as a normal user: key name and time only;

    • in the JSM portal: no details button.

  • Make one decision through each channel (panel, bulk, e-mail link, action link, Slack, approver portal, Web API). The panel and Activity show the right marker (none for the panel), the CSV "Channel" column matches, and a webhook receiver gets channel.

API keys

  • With the global setting off, "Submit approval decisions" cannot be ticked, but an existing tick can be removed.

  • 0.0.0.0/0 with the privilege shows the warning in the form and the flag on the list; the History tab shows the privilege change.

  • The key form accepts the decision privilege with every performer origin, and refuses "User from API" without a group and "Work item field User" without a field, even without the decision privilege.

Regression of the fixes

  • An approval creator excluded from a group step cannot decide on it, even by replaying the panel request with their own token.

  • An archived delegation cannot be used, even by sending its id directly.

  • After a moderator decides on a vote step, reassigning another step of the same approval keeps the moderator's decision on the vote step.

  • A delegate votes on a group step and then another member votes; the delegate's entry still opens its delegation details.

  • Bulk decide with "vote" on a user step is refused, and the step is unchanged.

  • A step's "comment required" applies only while the matching global comment setting is on, in every channel including the panel. Turn the global setting off with the step flag still set: the panel no longer asks for a comment, matching e-mail links, Slack and the Web API.

  • A required comment is enforced:

    • in the approver portal, for Approve and for Consent (the form asks for the comment before submitting);

    • for a delegate in the panel;

    • in Slack;

    • with withComment=false sent to the panel endpoint.

  • On an approval with two active vote steps, voting options are checked against the step voted on.

  • With work item permissions enabled, a user without the "view approvals" permission cannot bulk decide from an issue view.

  • On a Jira Web API create with a fixed performer who never opened the app, there is no 500.

  • In Confluence, with a "User" key whose fixed user never opened the app, listing the approvals of a space with approvals on several pages returns those on pages the user can read; there is no 403 or 500.

  • A delegate receives an e-mail action link for someone else's user step, then the delegator edits the delegation (for example its end date). Clicking the link shows that the delegation no longer applies, and nothing is recorded.

  • A delegate clicks an e-mail link for a group step after the delegator already voted: the page shows that the delegation no longer applies, not a generic error.