Business Sign-off for Jira Data Center
| Requirement | Version |
|---|---|
| Jira Data Center | 10.3.0 – 10.7.x |
| Java | 17 (OpenJDK or Oracle JDK) |
| Databases | PostgreSQL 13+, MySQL 8.0+, Microsoft SQL Server 2019+, Oracle 19c+ |
| Browsers | Chrome 90+, Firefox 90+, Safari 15+, Edge 90+ |
| Cluster | Supports 1-node through multi-node Data Center deployments |
Business Sign-off is distributed as a ZIP file containing the plugin JAR, a detached signature, a vendor certificate, and documentation. Extract all files from the ZIP to a folder on your Jira server:
| File | Purpose |
|---|---|
business-signoff-X.Y.Z.jar |
The plugin |
business-signoff-X.Y.Z.sig |
Detached signature (proves the JAR is authentic) |
cahabaforge-cert.pem |
Cahaba Forge vendor certificate (one-time trust store import) |
admin-guide-X.Y.Z.html |
This guide (configuration, REST API reference, troubleshooting) |
user-guide-X.Y.Z.html |
End-user guide (approving/rejecting issues, viewing sign-off status) |
INSTALL.html |
Quick-start installation instructions |
UPM does not support app signing in Jira 10.3 or 10.4. Skip directly to Step 2.
Jira 10.5 through 10.7 ships with UPM app signing support, but it is disabled by default during a grace period. The plugin will install without the certificate unless your administrator has explicitly enabled signature verification.
You only need to install the certificate if:
-Datlassian.upm.signature.check.disabled=false (or using the UPM configuration), orIf neither applies, skip to Step 2. Otherwise, follow the certificate installation steps below.
Starting with Jira 11, UPM app signing is enabled by default. You must install the Cahaba Forge certificate before uploading the plugin.
Only required on first install — skip if already done.
cahabaforge-cert.pem from the ZIP to your Jira server<jira-home>/upmconfig/truststore/
mkdir -p <jira-home>/upmconfig/truststore
chmod 555 <jira-home>/upmconfig/
chmod 555 <jira-home>/upmconfig/truststore/
chmod 444 <jira-home>/upmconfig/truststore/cahabaforge-cert.pem
Tip: Not sure where
<jira-home>is? In Jira, go to Administration > System > System Info and look for the Jira Home Directory entry.
Only required once — skip if you already see the "Upload app" button.
Starting with Jira 9.4.17 / 10.x, the upload button is hidden by default.
<jira-install>/bin/setenv.sh (Linux) or setenv.bat (Windows)JVM_SUPPORT_RECOMMENDED_ARGS and append this flag:
-Dupm.plugin.upload.enabled=true
For example, if the existing line is:
JVM_SUPPORT_RECOMMENDED_ARGS="-Xss512k"
Change it to:
JVM_SUPPORT_RECOMMENDED_ARGS="-Xss512k -Dupm.plugin.upload.enabled=true"
Tip: Atlassian recommends disabling this after upload for security. You can remove the parameter and restart after installing. You will need to re-enable it again for future upgrades.
business-signoff-X.Y.Z.jar from the ZIPbusiness-signoff-X.Y.Z.sig
For future upgrades, only Step 3 is needed. The certificate (Step 1) persists across upgrades. If you removed the -Dupm.plugin.upload.enabled=true parameter after the initial install (as recommended in Step 2), you will need to re-enable it before uploading the new version, then remove it again afterward.
If you prefer not to use the UI upload:
business-signoff-X.Y.Z.jar to: <jira-shared-home>/plugins/installed-plugins/Business Sign-off uses a custom license framework. Licenses are managed through a dedicated License Administration page, separate from the Atlassian UPM.
/plugins/servlet/signoff/admin/licenseOnly Jira System Administrators can access this page.
| State | Behavior |
|---|---|
| Valid | All features enabled (active paid license) |
| Evaluation | All features enabled during the trial period. On first install, the plugin automatically activates a 30-day auto trial. A signed trial license also returns this state. |
| Grace Period | Paid license has expired but within the 30-day grace period; all features remain enabled with a warning banner. Grace period applies to paid licenses only — expired trials go directly to Expired. |
| User Mismatch Grace | Active user count exceeds licensed seats; within the 30-day grace period with a warning |
| Expired | License expired and grace period ended (or trial expired); plugin enters read-only mode — existing data is preserved but write operations are blocked |
| User Mismatch | Seat overage past grace period; read-only mode |
| None | No license installed and no auto trial active; plugin is non-functional (fail-closed) |
| Invalid | License key is present but cannot be validated (bad signature, wrong server ID, etc.) |
The plugin immediately reflects the new license state. No restart required.
When a signed license is installed, the license details table shows:
The plugin sends email notifications to Jira system administrators when license issues arise:
Access the global configuration page from:
/plugins/servlet/signoff/admin/global-configOnly Jira System Administrators can access this page.
Toggle the Enable Plugin switch to activate or deactivate Business Sign-off globally. When disabled:
Finishing Mode: When disabling the plugin globally, you are prompted about pending approvals across all projects. You can choose to:
After enabling the plugin for the first time (or re-enabling after an extended period), a full Jira re-index is required so that the BSO - Status custom field is indexed for all existing issues. Without this re-index, JQL queries using "BSO - Status" IS EMPTY or IS NOT EMPTY will not return pre-existing issues.
To run a re-index:
Set the global default percentage of approvals required for an issue to pass:
| Threshold | Meaning |
|---|---|
| 50% | Simple majority |
| 66% | Two-thirds majority |
| 75% | Three-quarters majority |
| 90% | Near-unanimous |
| 100% | Unanimous |
Allow project override: When checked, project administrators can set a different threshold for their project, subject to the minimum threshold floor.
Minimum threshold: Sets the lowest threshold a project can configure (prevents projects from setting an inappropriately low bar).
Configure which issue participants cannot serve as approvers:
Allow project override: When checked, project administrators can set different SoD rules for their project.
SoD violations are enforced at the API level — the plugin will reject any attempt to add a restricted user as an approver or record their decision. Violations are logged in the audit trail with a FAIL SoD result.
Configure when approvers must provide a comment with their decision:
| Option | Behavior |
|---|---|
| All decisions | Comment required for both approvals and rejections |
| Approvals only | Comment required only when approving |
| Rejections only | Comment required only when rejecting |
| Never | Comments are optional |
Allow project override: When checked, project administrators can set different comment requirements.
Restrict who can serve as an approver across the instance:
Mode:
Filters (OR logic — user must match at least one enabled filter):
Fail-open safety: If filters are misconfigured (e.g., selected role doesn't exist in a project, or no users match), the plugin falls back to allowing all users with EDIT permission. This prevents approval deadlocks.
Allow project override: When checked, project administrators can configure different eligible approver filters.
Configure who can add and remove approvers on issues:
Note: At least one of these checkboxes must be selected. Workflow automation (post-functions, Automation for Jira) and system automation accounts can always add and remove approvers regardless of these settings.
Enable detailed DEBUG-level logging for troubleshooting. Debug logging automatically expires after the configured duration to prevent excessive log growth.
Toggle on, set the expiry duration (5 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 8 hours, or 24 hours), and reproduce the issue. View logs in Jira's application log (atlassian-jira.log). All plugin log entries use the io.cahaba.jira.signoff logger prefix.
Export approval history data to CSV format for offline analysis or auditor delivery.
Exports are generated as background tasks and tracked in the database. CSV files are written to the Jira shared home directory and are accessible from any cluster node.
Access project-level settings from:
Only Project Administrators can access project configuration.
Toggle Business Sign-off on or off for a specific project. When disabled:
Finishing Mode: When disabling a project, you are prompted about pending approvals. You can choose to:
Separate from the Enable toggle, the Require Approvals switch controls whether approval workflows are enforced for the project. When disabled:
Both the Enable and Require Approvals toggles must be on for full approval enforcement.
Choose which issues show the Business Sign-off panel:
Email notification preferences are configured at the project level:
If allowed by the global configuration, project administrators can override:
Settings left at their default (null) inherit the global value. Explicitly set values override the global default.
For automated approval enforcement, add workflow functions to your project's workflow:
Add the BSO - Approvers and BSO - Status custom fields to your screens:
SoD is a key compliance control. Here's how to set it up for different compliance frameworks:
Recommended configuration:
Recommended configuration:
SoD is enforced at the API level. To verify it's working:
sodResult: FAIL
Business Sign-off uses Jira's built-in mail system. Ensure your Jira instance has outgoing mail configured:
The plugin sends notifications using Velocity email templates. No additional email configuration is needed beyond Jira's standard mail setup.
| Event | Recipients | Configuration |
|---|---|---|
| Approver added | The added approver | Toggle on/off |
| Approval requested (bulk) | All pending approvers | Triggered via REST API or post-function |
| Approval reminder | Pending approvers | Triggered via REST API |
| Decision made | Reporter, Assignee | Configurable per outcome (Approved/Rejected/Both/Never) |
| Outcome reached | Reporter, Assignee | Configurable per outcome |
On any issue with Business Sign-off enabled:
The CSV includes all audit fields: issue key, project, action, actor, target, timestamps, SoD result, comments, reporter/assignee snapshots, and integrity hashes.
Each audit record includes a SHA-256 hash computed from the record's key fields. This hash allows you to verify that records have not been tampered with. To validate:
Business Sign-off exposes a REST API that enables automation, scripting, and integration with external tools. All endpoints use the base path:
/rest/bso-signoff/1.0
All endpoints require an authenticated Jira user. Use one of:
username:passwordBearer <token># Basic auth
curl -u admin:password https://jira.example.com/rest/bso-signoff/1.0/...
# Personal access token
curl -H "Authorization: Bearer ABCDEF123456" https://jira.example.com/rest/bso-signoff/1.0/...
An important distinction when working with the API:
JIRAUSER10001 or jsmith). Used in all userKey fields in the REST API.jsmith). Used when setting the BSO - Approvers custom field via Jira's standard REST API.In most Jira installations, the user key and username are the same value. However, they can differ if a username was renamed after account creation. When in doubt, use Jira's user search API to look up the correct user key:
curl -u admin:password \
"https://jira.example.com/rest/api/2/user/search?username=jsmith"
All endpoints return errors in a consistent format:
{
"success": false,
"error": "Human-readable error message"
}
When the plugin license is expired or has a seat overage past the grace period, all write operations (POST, PUT, DELETE) return 403 Forbidden with the message "Plugin is in read-only mode due to license status." Read operations (GET) continue to work normally.
These endpoints manage the list of approvers assigned to an issue.
Retrieve all approvers for an issue.
GET /rest/bso-signoff/1.0/issue/{issueKey}/approvers
Permissions: BROWSE_PROJECTS on the issue.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers"
Response (200 OK):
{
"approvers": [
{
"id": 101,
"userKey": "jsmith",
"displayName": "Jane Smith",
"status": "APPROVED",
"comment": "Looks good",
"createdDate": "2026-01-15T10:30:00.000+0000",
"decisionDate": "2026-01-16T14:22:00.000+0000"
},
{
"id": 102,
"userKey": "mbrown",
"displayName": "Mike Brown",
"status": "PENDING",
"comment": null,
"createdDate": "2026-01-15T10:30:00.000+0000",
"decisionDate": null
}
],
"count": 2
}
| Status Code | Meaning |
|---|---|
200 OK |
Approvers retrieved |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Insufficient permissions |
404 Not Found |
Issue not found |
Add one approver to an issue.
POST /rest/bso-signoff/1.0/issue/{issueKey}/approvers
Permissions: Caller must be allowed to manage approvers (configured in global settings: reporter, assignee, or admin). Write operations blocked in license read-only mode.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
userKey |
string | Yes | The Jira user key of the person to add as an approver |
Example:
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{"userKey": "jsmith"}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers"
Response (201 Created):
{
"success": true,
"message": "Approver added",
"approver": {
"id": 103,
"userKey": "jsmith",
"displayName": "Jane Smith",
"status": "ADDED",
"createdDate": "2026-02-20T09:15:00.000+0000"
}
}
The response includes a Location header pointing to the approvers list endpoint.
| Status Code | Meaning |
|---|---|
201 Created |
Approver added |
400 Bad Request |
Validation failed (user not found, not eligible, SoD violation) |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Insufficient permissions or license read-only |
404 Not Found |
Issue not found |
409 Conflict |
User is already an approver |
Add multiple approvers in a single operation with optional email notification.
POST /rest/bso-signoff/1.0/issue/{issueKey}/approvers/bulk
Permissions: Same as single add. Write operations blocked in license read-only mode.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
userKeys |
array of strings | Yes | Jira user keys to add (maximum 50) |
notifyMode |
integer | No | 0 = no emails (default), 1 = individual email to each approver, 2 = single consolidated email to all |
Example:
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"userKeys": ["jsmith", "mbrown", "agarcia"],
"notifyMode": 1
}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/bulk"
Response (200 OK):
{
"added": ["jsmith", "mbrown"],
"skipped": {
"alreadyApprover": ["agarcia"],
"ineligible": [],
"notFound": []
},
"addedCount": 2,
"skippedCount": 1,
"notificationsSent": 2
}
The response tells you exactly which users were added and why any were skipped. This is useful for automation scripts that need to handle partial success.
| Status Code | Meaning |
|---|---|
200 OK |
Bulk operation completed (check addedCount and skippedCount) |
400 Bad Request |
Empty list or more than 50 user keys |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Insufficient permissions or license read-only |
404 Not Found |
Issue not found |
Remove an approver from an issue.
DELETE /rest/bso-signoff/1.0/issue/{issueKey}/approvers/{userKey}
Permissions: Caller must be allowed to manage approvers. Decided approvers (APPROVED/REJECTED) can only be removed by Jira system admins, Jira admins, or project admins. Write operations blocked in license read-only mode.
Example:
curl -u admin:password -X DELETE \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/jsmith"
Response (204 No Content): Empty body on success.
| Status Code | Meaning |
|---|---|
204 No Content |
Approver removed |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Insufficient permissions, issue completed, or license read-only |
404 Not Found |
Issue or approver not found |
Note: Approvers cannot be removed from issues in DONE or CLOSED status.
Check whether approvers can be modified on an issue. Useful for automation scripts to pre-check before attempting changes.
GET /rest/bso-signoff/1.0/issue/{issueKey}/approvers/can-modify
Permissions: BROWSE_PROJECTS on the issue.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/can-modify"
Response (200 OK):
{
"canModify": true,
"isEnabled": true,
"isCompleted": false,
"canManageApprovers": true
}
If canModify is false, the response includes a reason field explaining why (e.g., issue completed, license read-only, permission denied).
These endpoints record and manage approval decisions.
Record an approval, rejection, or withdrawal for an approver.
POST /rest/bso-signoff/1.0/issue/{issueKey}/approvers/{userKey}/decision
Permissions: The authenticated user must be the approver identified by {userKey}. Write operations blocked in license read-only mode.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | Yes | "APPROVED", "REJECTED", or "PENDING" (withdrawal) |
comment |
string | No | Decision comment (max 450 characters). May be required by configuration. |
Example — Approve with comment:
curl -u jsmith:password -X POST \
-H "Content-Type: application/json" \
-d '{
"status": "APPROVED",
"comment": "Reviewed and approved. Meets all acceptance criteria."
}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/jsmith/decision"
Example — Reject:
curl -u jsmith:password -X POST \
-H "Content-Type: application/json" \
-d '{
"status": "REJECTED",
"comment": "Missing unit tests for the error handling path."
}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/jsmith/decision"
Example — Withdraw a previous decision:
curl -u jsmith:password -X POST \
-H "Content-Type: application/json" \
-d '{
"status": "PENDING",
"comment": "Withdrawing approval pending re-review of security fix."
}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/jsmith/decision"
Response (200 OK):
{
"success": true,
"message": "Decision recorded",
"approver": {
"id": 101,
"userKey": "jsmith",
"status": "APPROVED",
"comment": "Reviewed and approved. Meets all acceptance criteria.",
"decisionDate": "2026-02-20T14:30:00.000+0000"
}
}
| Status Code | Meaning |
|---|---|
200 OK |
Decision recorded |
400 Bad Request |
Invalid status value, comment required but missing, comment too long, or invalid transition |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Not the approver, or license read-only |
404 Not Found |
Issue or approver not found |
Notes:
PENDING) always require a comment.PENDING or ADDED status.Check comment requirements and the current user's approver status for an issue. Useful for building custom UIs.
GET /rest/bso-signoff/1.0/issue/{issueKey}/approvers/decision-config
Permissions: BROWSE_PROJECTS on the issue.
Example:
curl -u jsmith:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/decision-config"
Response (200 OK):
{
"commentRequiredForApproval": false,
"commentRequiredForRejection": true,
"isApprover": true,
"currentStatus": "PENDING",
"currentComment": null
}
Send email notifications and request re-reviews.
Send notification emails to approvers, or request a re-review (which resets decisions and notifies).
POST /rest/bso-signoff/1.0/issue/{issueKey}/approvers/notify
Permissions: Caller must be the issue reporter, assignee, Jira admin, or project admin. Write operations blocked in license read-only mode.
Request Body (action-based format):
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | Yes | "SEND_NOTIFICATION" or "REQUEST_RE_REVIEW" |
note |
string | No | Note to include with re-review request |
Example — Send reminder to pending approvers:
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{"action": "SEND_NOTIFICATION"}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/notify"
Example — Request re-review (resets all decisions):
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"action": "REQUEST_RE_REVIEW",
"note": "Scope changed after the security review. Please re-review."
}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/notify"
Response (200 OK):
{
"success": true,
"notifiedCount": 3,
"resetCount": 2,
"approversNotified": ["Jane Smith", "Mike Brown", "Ana Garcia"],
"lastNotifiedDate": "2026-02-20T15:00:00.000+0000",
"message": "Notifications sent"
}
| Status Code | Meaning |
|---|---|
200 OK |
Notifications sent |
400 Bad Request |
No approvers to notify or no decisions to reset |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Insufficient permissions or license read-only |
404 Not Found |
Issue not found |
Check notification counts and whether the current user can send notifications. Useful for pre-checking before calling the notify endpoint.
GET /rest/bso-signoff/1.0/issue/{issueKey}/approvers/notify-status
Permissions: BROWSE_PROJECTS on the issue.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/notify-status"
Response (200 OK):
{
"totalApprovers": 4,
"addedCount": 0,
"pendingCount": 2,
"rejectedCount": 1,
"approvedCount": 1,
"decidedCount": 2,
"neverNotifiedCount": 0,
"previouslyNotifiedCount": 2,
"lastNotifiedDate": "2026-02-19T10:00:00.000+0000",
"canNotify": true
}
Get the overall approval status, approver counts, and license information for an issue.
GET /rest/bso-signoff/1.0/issue/{issueKey}/status
Permissions: BROWSE_PROJECTS on the issue.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/status"
Response (200 OK):
{
"status": "Awaiting Decisions",
"threshold": 75,
"approverCount": 4,
"addedCount": 0,
"approvedCount": 2,
"rejectedCount": 0,
"pendingCount": 2
}
When there are license issues, the response includes additional fields:
{
"status": "Awaiting Decisions",
"threshold": 75,
"approverCount": 4,
"approvedCount": 2,
"rejectedCount": 0,
"pendingCount": 2,
"addedCount": 0,
"licenseWarning": "expiring_soon",
"licenseDaysRemaining": 7,
"licenseReadOnly": false
}
Overall status values:
| Status | Meaning |
|---|---|
"Awaiting Decisions" |
Some approvers have not yet decided |
"Approval Passed" |
Approval threshold met |
"Approval Failed" |
Threshold cannot be reached |
"No Approvers" |
No approvers assigned |
Retrieve the approval history for an issue with pagination and optional filtering.
GET /rest/bso-signoff/1.0/issue/{issueKey}/history
Permissions: BROWSE_PROJECTS on the issue.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 |
Page number (1-based) |
pageSize |
integer | 20 |
Items per page (max 100) |
filter |
string | "all" |
"all" for all entries, or "decisions" for decision-only entries |
Example — Page 1, decisions only:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/history?page=1&pageSize=10&filter=decisions"
Response (200 OK):
{
"summary": {
"totalApprovers": 3,
"pendingCount": 1,
"approvedCount": 1,
"rejectedCount": 1,
"currentApprovalPercentage": 33,
"projectThreshold": 75,
"projectThresholdDisplay": "75%",
"overallStatus": "AWAITING_DECISIONS",
"overallStatusDisplay": "Awaiting Decisions",
"firstDecisionDate": "2026-02-15T10:00:00.000+0000",
"lastDecisionDate": "2026-02-18T14:30:00.000+0000",
"lastNotificationDate": "2026-02-14T09:00:00.000+0000"
},
"entries": [
{
"id": 501,
"timestamp": "2026-02-18T14:30:00.000+0000",
"timestampFormatted": "2 days ago",
"timestampAbsolute": "18/Feb/26 2:30 PM",
"action": "DECISION_APPROVED",
"actorUserKey": "jsmith",
"actorDisplayName": "Jane Smith",
"actorEmail": "jsmith@example.com",
"targetUserKey": "jsmith",
"targetDisplayName": "Jane Smith",
"targetEmail": "jsmith@example.com",
"previousValue": "PENDING",
"newValue": "APPROVED",
"comment": "Looks good",
"projectKey": "PROJ",
"issueKey": "PROJ-123",
"issueSummary": "Implement login page redesign",
"issueType": "Story",
"issuePriority": "Major",
"issueStatus": "In Review",
"reporterUserKey": "jdoe",
"reporterDisplayName": "John Doe",
"assigneeUserKey": "developer1",
"assigneeDisplayName": "Dev User",
"sodResult": null,
"recordHash": "a1b2c3d4...",
"actionDescription": "Jane Smith approved"
}
],
"pagination": {
"page": 1,
"pageSize": 10,
"totalItems": 5,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false,
"filter": "decisions"
}
}
History action types:
| Action | Description |
|---|---|
APPROVER_ADDED |
User added as approver |
APPROVER_REMOVED |
User removed as approver |
DECISION_APPROVED |
Approver approved |
DECISION_REJECTED |
Approver rejected |
DECISION_CHANGED |
Approver changed a previous decision |
DECISION_RESET |
Decision reset to PENDING |
STATUS_CHANGED |
Overall issue approval status changed |
APPROVERS_NOTIFIED |
Notification sent to approvers |
NOTIFICATION_SENT |
Individual email notification |
REMINDER_SENT |
Reminder email |
RE_REVIEW_REQUESTED |
Re-review requested with decision reset |
CONFIG_CHANGED |
Configuration changed |
Search for users who are eligible to be added as approvers. These endpoints apply the configured filters (eligible approver mode, project roles, user groups) and Segregation of Duties rules automatically.
Search for eligible approvers in the context of an existing issue. Automatically excludes users who are already approvers, fail SoD checks, or are not in eligible roles/groups.
GET /rest/bso-signoff/1.0/issue/{issueKey}/eligible-approvers/search
Permissions: BROWSE_PROJECTS on the issue, plus caller must be able to add approvers.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
string | (empty) | Search text (matches username, display name, email) |
maxResults |
integer | 10 |
Maximum results to return (1–50) |
startAt |
integer | 0 |
Offset for pagination (browse mode) |
Example — Search for users matching "smith":
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/eligible-approvers/search?query=smith&maxResults=10"
Example — Browse all eligible approvers (no search query):
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/eligible-approvers/search?maxResults=20&startAt=0"
Response (200 OK):
{
"users": [
{
"key": "jsmith",
"name": "jsmith",
"displayName": "Jane Smith",
"avatarUrl": "https://jira.example.com/secure/useravatar?ownerId=jsmith"
}
],
"total": 1,
"hasMore": false
}
Use hasMore to determine whether to fetch the next page with a higher startAt value.
Search for eligible approvers in the context of a project, before an issue exists. Use this when building automation that pre-populates approvers at issue creation time.
GET /rest/bso-signoff/1.0/project/{projectKey}/eligible-approvers/search
Permissions: BROWSE_PROJECTS on the project.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
string | (empty) | Search text |
maxResults |
integer | 10 |
Maximum results (1–50) |
startAt |
integer | 0 |
Offset for pagination |
reporter |
string | (none) | Reporter user key — used for SoD filtering |
assignee |
string | (none) | Assignee user key — used for SoD filtering |
Example — Search with SoD context:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/project/PROJ/eligible-approvers/search?query=smith&reporter=jdoe&assignee=mbrown"
Response: Same format as the issue-based search endpoint above.
Note: Since no issue exists yet, pass reporter and assignee as query parameters so the endpoint can apply SoD rules correctly. If omitted, SoD filtering is skipped.
Pre-check whether a specific user can be added as an approver to an issue. Returns a clear valid flag and reason if not.
GET /rest/bso-signoff/1.0/issue/{issueKey}/approvers/validate?userKey={userKey}
Permissions: Caller must be able to add approvers.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/PROJ-123/approvers/validate?userKey=jsmith"
Response (200 OK):
{
"valid": true,
"userKey": "jsmith",
"displayName": "Jane Smith"
}
If the user cannot be added, the response includes a 400 status with an error message explaining why (e.g., SoD violation, not eligible, already an approver).
Retrieve the full global plugin configuration. Useful for automation that needs to understand current settings.
GET /rest/bso-signoff/1.0/config/global
Permissions: SYSTEM_ADMIN global permission.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/config/global"
Response (200 OK):
{
"pluginEnabled": true,
"globalFinishingMode": false,
"sodAssigneeCannotApprove": true,
"sodReporterCannotApprove": true,
"sodAllowProjectOverride": false,
"approverMgmtAllowReporter": true,
"approverMgmtAllowAssignee": true,
"approverMgmtAllowAdministrators": true,
"commentRequiredOn": "REJECT_ONLY",
"commentRequiredAllowProjectOverride": true,
"approvalThreshold": 75,
"approvalThresholdAllowProjectOverride": true,
"approvalThresholdMinimum": 50,
"eligibleApproverMode": "ALL_USERS",
"eligibleApproverFilterByRole": false,
"eligibleApproverRoleIds": "",
"eligibleApproverFilterByGroup": false,
"eligibleApproverGroupNames": "",
"eligibleApproverAllowProjectOverride": false
}
Update global plugin configuration settings.
PUT /rest/bso-signoff/1.0/config/global
Permissions: SYSTEM_ADMIN global permission.
Request Body: Include only the fields you want to update. Fields use the same names as the GET response.
Example — Change threshold and SoD settings:
curl -u admin:password -X PUT \
-H "Content-Type: application/json" \
-d '{
"approvalThreshold": 100,
"sodAssigneeCannotApprove": true,
"sodReporterCannotApprove": true
}' \
"https://jira.example.com/rest/bso-signoff/1.0/config/global"
Response (200 OK):
{
"success": true,
"message": "Configuration updated"
}
| Status Code | Meaning |
|---|---|
200 OK |
Configuration updated |
400 Bad Request |
Validation error |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Not a system admin |
Retrieve the effective configuration for a project, with global overrides resolved.
GET /rest/bso-signoff/1.0/config/project/{projectKey}
Permissions: ADMINISTER_PROJECTS on the project.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/config/project/PROJ"
Response (200 OK):
{
"projectKey": "PROJ",
"projectId": 10001,
"enabled": true,
"panelVisibility": "ALL",
"approvalRequired": true,
"requiredIssueTypeIds": [],
"approvalThreshold": 75,
"approvalThresholdRaw": null,
"approvalThresholdAllowProjectOverride": true,
"commentRequiredOn": "REJECT_ONLY",
"commentRequiredAllowProjectOverride": true,
"sodAssigneeCannotApprove": true,
"sodReporterCannotApprove": true,
"emailApproverOnAdd": true,
"emailRequestorOnOutcome": "APPROVED_OR_REJECTED",
"emailRequestorOnDecision": "NEVER",
"emailAssigneeOnOutcome": "APPROVED_OR_REJECTED",
"emailAssigneeOnDecision": "NEVER",
"eligibleApproverMode": "ALL_USERS",
"eligibleApproverFilterByRole": false,
"eligibleApproverRoleIds": "",
"eligibleApproverFilterByGroup": false,
"eligibleApproverGroupNames": ""
}
Notes:
Raw show the project's own override value (null means "inherit from global"). Fields without Raw show the resolved effective value.panelVisibility values: ALL (show on all issue types) or SELECTED (show only on issue types listed in requiredIssueTypeIds)APPROVED_ONLY, REJECTED_ONLY, APPROVED_OR_REJECTED, NEVEReligibleApproverAllowProjectOverride global settingExport approval history across projects and date ranges for compliance reporting.
Start an async background export task.
POST /rest/bso-signoff/1.0/audit/export/start
Permissions: SYSTEM_ADMIN global permission. Write operations blocked in license read-only mode.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
projectKeys |
array of strings | Yes | Project keys to include (max 50) |
fromDate |
string | Yes | Start date in yyyy-MM-dd format |
toDate |
string | Yes | End date in yyyy-MM-dd format (max 365-day range) |
decisionsOnly |
boolean | No | When true, export only decision records (approvals/rejections). Default: true |
Example:
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"projectKeys": ["PROJ", "SEC"],
"fromDate": "2026-01-01",
"toDate": "2026-02-20",
"decisionsOnly": false
}' \
"https://jira.example.com/rest/bso-signoff/1.0/audit/export/start"
Response (200 OK):
{
"taskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "RUNNING"
}
Save the taskId — you need it to check progress and download the file.
| Status Code | Meaning |
|---|---|
200 OK |
Export started |
400 Bad Request |
Invalid dates, too many projects, or date range exceeds 365 days |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Not a system admin, or license read-only |
503 Service Unavailable |
Too many concurrent exports (max 5) |
Poll the status of a running export task.
GET /rest/bso-signoff/1.0/audit/export/status/{taskId}
Permissions: SYSTEM_ADMIN global permission.
Example:
curl -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/audit/export/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Response — In progress:
{
"taskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "RUNNING",
"progress": 65
}
Response — Completed:
{
"taskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "COMPLETED",
"progress": 100,
"downloadUrl": "/rest/bso-signoff/1.0/audit/export/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Response — Failed:
{
"taskId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "FAILED",
"errorMessage": "No audit records found for the specified criteria"
}
Download the completed CSV export file.
GET /rest/bso-signoff/1.0/audit/export/download/{taskId}
Permissions: SYSTEM_ADMIN global permission.
Example:
curl -u admin:password -o audit-export.csv \
"https://jira.example.com/rest/bso-signoff/1.0/audit/export/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
The file is streamed as text/csv; charset=UTF-8 with a UTF-8 BOM. The Content-Disposition header includes a date-stamped filename.
| Status Code | Meaning |
|---|---|
200 OK |
CSV file downloaded |
400 Bad Request |
Export task is not yet completed |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Not a system admin |
404 Not Found |
Task not found or export file missing |
Below are end-to-end examples for common automation scenarios.
Use Jira's standard REST API to create an issue with the BSO - Approvers custom field already populated. The plugin's sync listener detects the field values on the ISSUE_CREATED event and automatically creates the approver records.
Important: When setting the BSO - Approvers field via Jira's issue create API, pass usernames (not user keys) in the "name" field — this is what Jira's MultiUserCFType expects.
# Find the custom field ID for "BSO - Approvers"
# (check Jira Admin > Custom Fields, or use the REST API)
FIELD_ID="customfield_10100"
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"fields": {
"project": {"key": "PROJ"},
"issuetype": {"name": "Story"},
"summary": "Implement login page redesign",
"assignee": {"name": "developer1"},
"'$FIELD_ID'": [
{"name": "jsmith"},
{"name": "mbrown"},
{"name": "agarcia"}
]
}
}' \
"https://jira.example.com/rest/api/2/issue"
The plugin automatically adds all three users as approvers when the issue is created. No separate API call is needed.
Note: To find your custom field ID, go to Jira Administration > Custom Fields, find "BSO - Approvers", and note the numeric ID in the URL when you click Configure — the field ID is customfield_NNNNN.
If you prefer explicit control and visibility into which approvers were added vs. skipped, use the bulk add endpoint after creating the issue.
# Step 1: Create the issue
RESPONSE=$(curl -s -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"fields": {
"project": {"key": "PROJ"},
"issuetype": {"name": "Story"},
"summary": "Implement login page redesign"
}
}' \
"https://jira.example.com/rest/api/2/issue")
ISSUE_KEY=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['key'])")
# Step 2: Add approvers via bulk endpoint (uses user keys)
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"userKeys": ["jsmith", "mbrown", "agarcia"],
"notifyMode": 1
}' \
"https://jira.example.com/rest/bso-signoff/1.0/issue/$ISSUE_KEY/approvers/bulk"
This approach gives you the detailed response showing exactly which users were added and which were skipped (and why).
Copy the approvers from a source issue to a target issue — useful for cloning approval boards across related issues.
SOURCE_ISSUE="PROJ-100"
TARGET_ISSUE="PROJ-200"
# Step 1: Get approvers from source issue
APPROVERS=$(curl -s -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/issue/$SOURCE_ISSUE/approvers")
# Step 2: Extract user keys
USER_KEYS=$(echo $APPROVERS | python3 -c "
import sys, json
data = json.load(sys.stdin)
keys = [a['userKey'] for a in data['approvers']]
print(json.dumps(keys))
")
# Step 3: Add to target issue
curl -u admin:password -X POST \
-H "Content-Type: application/json" \
-d "{\"userKeys\": $USER_KEYS, \"notifyMode\": 1}" \
"https://jira.example.com/rest/bso-signoff/1.0/issue/$TARGET_ISSUE/approvers/bulk"
A scripted workflow for exporting audit data and waiting for completion.
# Step 1: Start the export
RESPONSE=$(curl -s -u admin:password -X POST \
-H "Content-Type: application/json" \
-d '{
"projectKeys": ["PROJ", "SEC", "INFRA"],
"fromDate": "2026-01-01",
"toDate": "2026-03-31"
}' \
"https://jira.example.com/rest/bso-signoff/1.0/audit/export/start")
TASK_ID=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['taskId'])")
# Step 2: Poll until complete
while true; do
STATUS=$(curl -s -u admin:password \
"https://jira.example.com/rest/bso-signoff/1.0/audit/export/status/$TASK_ID")
STATE=$(echo $STATUS | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
if [ "$STATE" = "COMPLETED" ]; then
echo "Export complete. Downloading..."
break
elif [ "$STATE" = "FAILED" ]; then
echo "Export failed."
echo $STATUS
exit 1
fi
PROGRESS=$(echo $STATUS | python3 -c "import sys,json; print(json.load(sys.stdin).get('progress', 0))")
echo "Progress: ${PROGRESS}%"
sleep 5
done
# Step 3: Download the file
curl -u admin:password -o "audit-export-Q1-2026.csv" \
"https://jira.example.com/rest/bso-signoff/1.0/audit/export/download/$TASK_ID"
echo "Saved to audit-export-Q1-2026.csv"
| Status | Description |
|---|---|
ADDED |
Approver has been added but not yet notified |
PENDING |
Approver has been notified and is awaiting their decision |
APPROVED |
Approver has approved |
REJECTED |
Approver has rejected |
| Value | Meaning |
|---|---|
ALL |
Comment required for both approval and rejection |
APPROVAL_ONLY |
Comment required only when approving |
REJECT_ONLY |
Comment required only when rejecting |
NONE |
Comments are always optional |
| Value | Meaning |
|---|---|
APPROVED_ONLY |
Notify only on approval |
REJECTED_ONLY |
Notify only on rejection |
APPROVED_OR_REJECTED |
Notify on both approval and rejection |
NEVER |
Never send notifications |
Business Sign-off registers the following JQL functions:
bsoApprovers()Find issues where a specific user is an approver.
issue in bsoApprovers("username")
Returns all issues where the specified user is listed as an approver (any status: ADDED, PENDING, APPROVED, REJECTED).
bsoApproverStatus()Find issues where a specific user has a specific approver status.
issue in bsoApproverStatus("username", "STATUS")
Valid status values: ADDED, PENDING, APPROVED, REJECTED
Examples:
issue in bsoApproverStatus("jsmith", "APPROVED") — issues where jsmith has approvedissue in bsoApproverStatus("jsmith", "PENDING") — issues where jsmith hasn't decided yetbsoStatus() (Not Yet Functional)This JQL function is registered but currently returns empty results. Use the BSO - Status custom field for issue-level approval status queries instead:
"BSO - Status" = "Approval Passed"
"BSO - Status" = "Approval Failed"
"BSO - Status" = "Awaiting Decisions"
"BSO - Status" IS EMPTY
atlassian-jira.log for detailed error messagesissue in bsoApprovers("username"), issue in bsoApproverStatus("username", "APPROVED")bsoStatus() JQL function is registered but not yet functional — it returns empty results. Use the BSO - Status custom field in JQL instead (e.g., "BSO - Status" = "Approval Passed")atlassian-jira.log for entries with the io.cahaba.jira.signoff logger prefix
Cahaba Forge LLC | cahabaforge.com