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.
Generate a JWT token for CLI access using a GitHub PAT supplied in the request body
GitHub organization or user name
GitHub token for authentication
Successful token generation
JWT access token for Nullify API
Bad Request
Unauthorized
Forbidden
Internal Server Error
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.
Generate a JWT token for GitHub Actions or CLI access
GitHub organization or user name
GitHub token for authentication
Successful token generation
JWT access token for Nullify API
Bad Request
Unauthorized
Forbidden
Internal Server Error
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.
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.
Exchange a refresh token for a new access token
Refresh token cookie
Successful token refresh
Token expiration timestamp in milliseconds
Token expiration time in seconds
List of GitHub installation IDs
Bad Request
Unauthorized
Forbidden
Internal Server Error
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.
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).
Report whether the caller's address passes this tenant's IP allowlist
Caller network admission
IP allowlist unavailable
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 json for installing Nullify into a Bitbucket workspace
OK
Internal Server Error
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.
Callback endpoint for when the Bitbucket app is installed
No Content
No content
Internal Server Error
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.
Callback endpoint for when the Bitbucket app is enabled
No Content
No content
Internal Server Error
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.
Callback endpoint for when the Bitbucket app is disabled
No Content
No content
Internal Server Error
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.
Callback endpoint for when the Bitbucket app is installed
No Content
No content
Internal Server Error
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