For the complete documentation index, see llms.txt. This page is also available as Markdown.

Authentication

Authenticate API requests with a Nullify bearer token. Use a service account for automation, or CLI login for an interactive session.

Create a service account

In your tenant's settings, open API access, create an account, and copy its token to a secrets manager. The token is shown once. The Admin API also supports account creation.

Choose builtin:admin, builtin:editor, or builtin:viewer explicitly in the API. These are fixed built-in service-account roles; omitting the role in the creation API defaults to builtin:admin. Use a separate account for each automation.

curl --get 'https://api.<TENANT>.nullify.ai/sast/findings' \
  --header "Authorization: Bearer $NULLIFY_TOKEN" \
  --data-urlencode 'githubOwnerId=<OWNER_ID>' \
  --data-urlencode 'limit=20'

Provider-scoped endpoints also require the installation or owner identifier described by that endpoint's schema. A bearer token does not make an arbitrary tenant or repository identifier accessible.

Authorization and token lifetime

Missing or invalid authentication can return 401 Unauthorized; insufficient access can return 403 Forbidden. Role-permission enforcement is a tenant rollout setting and is off by default. Do not assume a viewer/editor token is a least-privilege boundary until Nullify has confirmed enforcement for your tenant. Tenant and provider access checks still apply independently.

That rollout caveat applies to ordinary SCM and service-account role enforcement. Managed SSO/Entra callers remain subject to resolved role permissions, and customer routes requiring identity administration—including directory and SCIM credential management, access activation and sign-in policy—remain permission-gated. Membership, lifecycle and network admission are separate checks. See Microsoft Entra SSO and SCIM for setup and acceptance.

Team-scope enforcement still requires the workspace's role-enforcement rollout, including for Microsoft users. Entra authentication alone does not enable team restrictions.

To rotate routinely, create a replacement account, update each consumer's stored token, and verify its requests. Coordinate invalidation of the old token with Nullify; creating a new account alone does not invalidate it. Interactive CLI credentials have a separate expiry and refresh flow.

GitHub token exchange

The CLI can exchange a GitHub credential for a Nullify token when running in GitHub Actions; see CI authentication. The exchange checks the supplied GitHub credential against the owner you name. The credential must belong to that owner's user account or to an active member of that organization, or be an installation token for it, and the owner must have the Nullify GitHub app installed. The resulting token is valid for one hour and carries the built-in editor role.

Exchange a GitHub token (POST)

New direct integrations should use POST /auth/github_token with owner (the GitHub user or organization login) and token (the GitHub credential) in the JSON body. A successful call returns accessToken, which you send as a bearer token on later API calls. It returns 400 when either field is missing or the credential does not verify for that owner. The endpoint needs no Nullify authentication, so never send GitHub credentials to an untrusted API host.

Get GitHub Token

POST

Generate a JWT token for CLI access using a GitHub PAT supplied in the request body

Body
ownerstringRequired

GitHub organization or user name

tokenstringRequired

GitHub token for authentication

Responses
200

Successful token generation

application/json
accessTokenstringRequired

JWT access token for Nullify API

POST/auth/github_token
POST /auth/github_token HTTP/1.1
Host: api.<TENANT>.nullify.ai
Content-Type: application/json
Accept: */*
Content-Length: 31

{
  "owner": "text",
  "token": "text"
}
{
  "accessToken": "text"
}

Exchange a GitHub token (GET, deprecated)

The legacy GET /auth/github_token?owner=...&token=... does the same exchange but puts the GitHub credential in the query string, where proxies and access logs can capture it. It is retained for older clients only; use the POST form.

Get GitHub Token

GET

Generate a JWT token for GitHub Actions or CLI access

Query parameters
ownerstringRequired

GitHub organization or user name

tokenstringRequired

GitHub token for authentication

Responses
200

Successful token generation

application/json
accessTokenstringRequired

JWT access token for Nullify API

GET/auth/github_token
GET /auth/github_token?owner=text&token=text HTTP/1.1
Host: api.<TENANT>.nullify.ai
Accept: */*
{
  "accessToken": "text"
}

Browser session endpoints

The remaining /auth operations support the Nullify web app's sign-in session. They use HttpOnly cookies (access_token, refresh_token) set on your tenant domain, not bearer tokens, and none of them needs an Authorization header. They are documented for completeness. Automation should use a service account or the GitHub token exchange instead.

Exchange an authorization code

Browser sign-in uses GET /auth/ui/login and GET /auth/ui/callback. The backend stores state and the PKCE verifier in an HttpOnly transaction cookie, validates both on the callback, sets the session cookies, and redirects to the application. GET /auth/whoami returns the signed-in identity. Browser JavaScript never exchanges a code or receives the PKCE verifier.

nullify-openapi-bundle.yaml

Refresh the access token

GET /auth/refresh_token exchanges the refresh_token cookie for a new access_token cookie and returns the same session summary as above. It takes no parameters and returns 401 when the cookie is missing, invalid or expired. The refresh cookie is left in place; how long it stays usable depends on your tenant's sign-in session settings, so be ready to sign in again when this call returns 401.

Refresh Access Token

GET

Exchange a refresh token for a new access token

cookie
refresh_tokenstringRequired

Refresh token cookie

Responses
200

Successful token refresh

application/json
expiresAtinteger · int64Required

Token expiration timestamp in milliseconds

expiresInintegerRequired

Token expiration time in seconds

installationIdsstring[]Required

List of GitHub installation IDs

GET/auth/refresh_token
GET /auth/refresh_token HTTP/1.1
Host: api.<TENANT>.nullify.ai
Accept: */*
{
  "expiresAt": 1,
  "expiresIn": 1,
  "installationIds": [
    "text"
  ]
}

Log out

GET /auth/logout clears the session cookies and returns 200 with no body. It does not revoke tokens already issued.

Logout

GET

Clear authentication cookies and log out the user

Responses
200

OK

No content

GET/auth/logout
GET /auth/logout HTTP/1.1
Host: api.<TENANT>.nullify.ai
Accept: */*
200

OK

No content

Check the IP allowlist

GET /auth/network/ip_allowed reports the address Nullify sees for your request (ip) and whether it passes your tenant's IP allowlist (allowed). It is unauthenticated on purpose, so a caller blocked by the allowlist can still find out why: a blocked request to the rest of the API returns a bare 403. It reveals only whether your own address is allowed, never the list, and returns 503 if the allowlist cannot be evaluated. Call it on your tenant's API host (api.<TENANT>.nullify.ai).

Check IP Allowlist

get

Report whether the caller's address passes this tenant's IP allowlist

Responses
200

Caller network admission

application/json
allowedbooleanRequired
ipstringRequired
get/auth/network/ip_allowed
GET /auth/network/ip_allowed HTTP/1.1
Host: api.<TENANT>.nullify.ai
Accept: */*
{
  "allowed": true,
  "ip": "text"
}

Platform callbacks (not for direct use)

The four Bitbucket lifecycle callbacks and the descriptor below are called by Atlassian when a workspace admin installs, enables, disables or removes the Nullify Bitbucket Connect app. They are not part of the integration surface for your own code: do not call them, and you do not need to. They exist in the API reference only because they are served on the public API host. They are available only on stacks with Bitbucket enabled. To connect Bitbucket, follow the Bitbucket connector instructions.

Bitbucket Connect descriptor

GET /core/bitbucket/descriptor returns the Connect app descriptor that Bitbucket reads when the app is installed: the app key, base URL, the lifecycle callback paths, the webhook events (repo:push, pullrequest:created, pullrequest:updated, pullrequest:comment_created), JWT authentication, and the scopes account, repository and pullrequest.

Bitbucket Connect Descriptor

get

Bitbucket connect descriptor json for installing Nullify into a Bitbucket workspace

Responses
200

OK

application/json
apiVersionintegerOptional
baseUrlstringRequired
contextsstring[]Optional
descriptionstringRequired
enableLicensingbooleanOptional
keystringRequired
namestringRequired
scopesstring[]Optional
versionstringOptional
get/core/bitbucket/descriptor
GET /core/bitbucket/descriptor HTTP/1.1
Host: api.<TENANT>.nullify.ai
Accept: */*
{
  "apiVersion": 1,
  "authentication": {
    "type": "text"
  },
  "baseUrl": "text",
  "contexts": [
    "text"
  ],
  "definitions": {
    "ANY_ADDITIONAL_PROPERTY": "anything"
  },
  "description": "text",
  "enableLicensing": true,
  "key": "text",
  "lifecycle": {
    "disabled": "text",
    "enabled": "text",
    "installed": "text",
    "uninstalled": "text"
  },
  "links": {
    "documentation": "text",
    "homepage": "text",
    "self": "text",
    "source": "text",
    "support": "text"
  },
  "modules": {
    "adminPages": [
      {
        "conditions": [
          "text"
        ],
        "icon": {
          "height": 1,
          "url": "text",
          "width": 1
        },
        "key": "text",
        "location": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "tooltip": {
          "i18n": "text",
          "value": "text"
        },
        "url": "text",
        "weight": 1
      }
    ],
    "configurePage": 1,
    "fileEditors": [
      {
        "conditions": [
          "text"
        ],
        "file_matches": {
          "extensions": [
            "text"
          ],
          "names": [
            "text"
          ]
        },
        "key": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "url": "text"
      }
    ],
    "fileViews": [
      {
        "conditions": [
          1
        ],
        "file_matches": {
          "extensions": [
            "text"
          ],
          "names": [
            "text"
          ]
        },
        "key": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "url": "text"
      }
    ],
    "generalPages": [
      {
        "conditions": [
          "text"
        ],
        "icon": {
          "height": 1,
          "url": "text",
          "width": 1
        },
        "key": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "url": "text",
        "weight": 1
      }
    ],
    "linkers": [
      {
        "conditions": [
          "text"
        ],
        "context": "text",
        "key": "text",
        "params": {},
        "regex": "text",
        "target": {},
        "type": "text",
        "url": "text"
      }
    ],
    "oauthCallback": {
      "key": "text",
      "url": "text"
    },
    "oauthConsumer": {
      "clientId": "text",
      "scopes": [
        "text"
      ]
    },
    "postInstallRedirect": {
      "key": "text",
      "url": "text"
    },
    "profileTabs": [
      {
        "conditions": [
          "text"
        ],
        "icon": {
          "height": 1,
          "url": "text",
          "width": 1
        },
        "key": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "url": "text",
        "weight": 1
      }
    ],
    "proxy": {
      "definitions": {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      },
      "links": {
        "ANY_ADDITIONAL_PROPERTY": "text"
      }
    },
    "repoPages": [
      {
        "conditions": [
          true
        ],
        "icon": {
          "height": 1,
          "url": "text",
          "width": 1
        },
        "key": "text",
        "location": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "tooltip": {
          "i18n": "text",
          "value": "text"
        },
        "url": "text",
        "weight": 1
      }
    ],
    "webCards": [
      {
        "conditions": [
          "text"
        ],
        "icon": {
          "height": 1,
          "url": "text",
          "width": 1
        },
        "key": "text",
        "location": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "tooltip": {
          "i18n": "text",
          "value": "text"
        },
        "url": "text",
        "weight": 1
      }
    ],
    "webItems": [
      true
    ],
    "webPanels": [
      {
        "conditions": [
          "text"
        ],
        "icon": {
          "height": 1,
          "url": "text",
          "width": 1
        },
        "key": "text",
        "location": "text",
        "name": {
          "i18n": "text",
          "value": "text"
        },
        "params": {},
        "tooltip": {
          "i18n": "text",
          "value": "text"
        },
        "url": "text",
        "weight": 1
      }
    ],
    "webhooks": [
      {
        "event": "text",
        "params": {},
        "url": "text"
      }
    ]
  },
  "name": "text",
  "scopes": [
    "text"
  ],
  "vendor": {
    "name": "text",
    "url": "text"
  },
  "version": "text"
}

Bitbucket app installed

POST /core/bitbucket/installed is called by Bitbucket after the app is installed on a workspace. It records the installation (client key, shared secret and public key sent by Bitbucket) and publishes an install event.

Bitbucket App Installed

post

Callback endpoint for when the Bitbucket app is installed

Body
baseApiUrlstringRequired
baseUrlstringRequired
clientKeystringRequired
eventTypestringRequired
keystringRequired
productTypestringRequired
publicKeystringRequired
sharedSecretstringRequired
Responses
204

No Content

No content

post/core/bitbucket/installed
POST /core/bitbucket/installed HTTP/1.1
Host: api.<TENANT>.nullify.ai
Content-Type: application/json
Accept: */*
Content-Length: 786

{
  "actor": {
    "account_id": "text",
    "type": "text",
    "uuid": "text"
  },
  "baseApiUrl": "text",
  "baseUrl": "text",
  "clientKey": "text",
  "eventType": "text",
  "key": "text",
  "principal": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  },
  "productType": "text",
  "publicKey": "text",
  "sharedSecret": "text",
  "user": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  }
}

No content

Bitbucket app enabled

POST /core/bitbucket/enabled is called when the app is re-enabled. It marks the installation as active again and publishes an unsuspend event. It returns 404 if the workspace has no recorded installation.

Bitbucket App Enabled

post

Callback endpoint for when the Bitbucket app is enabled

Body
baseApiUrlstringRequired
baseUrlstringRequired
clientKeystringRequired
eventTypestringRequired
keystringRequired
productTypestringRequired
publicKeystringRequired
Responses
204

No Content

No content

post/core/bitbucket/enabled
POST /core/bitbucket/enabled HTTP/1.1
Host: api.<TENANT>.nullify.ai
Content-Type: application/json
Accept: */*
Content-Length: 764

{
  "actor": {
    "account_id": "text",
    "type": "text",
    "uuid": "text"
  },
  "baseApiUrl": "text",
  "baseUrl": "text",
  "clientKey": "text",
  "eventType": "text",
  "key": "text",
  "principal": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  },
  "productType": "text",
  "publicKey": "text",
  "user": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  }
}

No content

Bitbucket app disabled

POST /core/bitbucket/disabled is called when the app is disabled. It marks the installation as suspended and publishes a suspend event. It returns 404 if the workspace has no recorded installation.

Bitbucket App Disabled

post

Callback endpoint for when the Bitbucket app is disabled

Body
baseApiUrlstringRequired
baseUrlstringRequired
clientKeystringRequired
eventTypestringRequired
keystringRequired
productTypestringRequired
publicKeystringRequired
Responses
204

No Content

No content

post/core/bitbucket/disabled
POST /core/bitbucket/disabled HTTP/1.1
Host: api.<TENANT>.nullify.ai
Content-Type: application/json
Accept: */*
Content-Length: 764

{
  "actor": {
    "account_id": "text",
    "type": "text",
    "uuid": "text"
  },
  "baseApiUrl": "text",
  "baseUrl": "text",
  "clientKey": "text",
  "eventType": "text",
  "key": "text",
  "principal": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  },
  "productType": "text",
  "publicKey": "text",
  "user": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  }
}

No content

Bitbucket app uninstalled

POST /core/bitbucket/uninstalled is called when the app is removed from a workspace. It marks the installation as suspended and publishes a delete event. It returns 404 if the workspace has no recorded installation.

Bitbucket App Uninstalled

post

Callback endpoint for when the Bitbucket app is installed

Body
baseApiUrlstringRequired
baseUrlstringRequired
clientKeystringRequired
eventTypestringRequired
keystringRequired
productTypestringRequired
publicKeystringRequired
sharedSecretstringRequired
Responses
204

No Content

No content

post/core/bitbucket/uninstalled
POST /core/bitbucket/uninstalled HTTP/1.1
Host: api.<TENANT>.nullify.ai
Content-Type: application/json
Accept: */*
Content-Length: 786

{
  "actor": {
    "account_id": "text",
    "type": "text",
    "uuid": "text"
  },
  "baseApiUrl": "text",
  "baseUrl": "text",
  "clientKey": "text",
  "eventType": "text",
  "key": "text",
  "principal": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  },
  "productType": "text",
  "publicKey": "text",
  "sharedSecret": "text",
  "user": {
    "created_on": "2026-01-01T00:00:00.000Z",
    "display_name": "text",
    "has_2fa_enabled": true,
    "is_active": true,
    "links": {
      "avatar": {
        "href": "text"
      },
      "html": {
        "href": "text"
      },
      "self": {
        "href": "text"
      }
    },
    "properties": {
      "ANY_ADDITIONAL_PROPERTY": "text"
    },
    "type": "text",
    "username": "text",
    "uuid": "text"
  }
}

No content

Retries

There is no published tenant-wide request quota or guaranteed Retry-After contract. Keep concurrency bounded, retry transient failures with exponential backoff and jitter, and honor Retry-After when returned. Do not automatically repeat a write that may already have succeeded: inspect the resulting resource or job before retrying.

Last updated