Upload an SBOM
Uploading creates a new version inside a product's environment. Each upload is a new version, so you do not overwrite anything.
Uploads are the one API call that does not use a JSON body. A file upload over GraphQL uses multipart/form-data, following the GraphQL multipart request spec. The sections below show exactly what that means for curl.
The request
A multipart upload sends three parts:
operations
The GraphQL mutation and its variables, as JSON. The file variable is set to null.
map
Tells the server which uploaded file fills which variable.
0
The SBOM file itself. The part name (0) matches the key in map.
Upload by product name
The simplest form. Name the product and environment, and the API resolves them.
curl https://api.interlynk.io/lynkapi \
-H "Authorization: Bearer $INTERLYNK_SECURITY_TOKEN" \
-F operations='{"query":"mutation uploadSbom($doc: Upload!, $projectGroupName: String, $projectName: String) { sbomUpload(input: { doc: $doc, projectGroupName: $projectGroupName, projectName: $projectName }) { errors } }","variables":{"doc":null,"projectGroupName":"payments-service","projectName":"default"}}' \
-F map='{"0":["variables.doc"]}' \
-F [email protected]A successful upload returns an empty errors list:
{
"data": {
"sbomUpload": {
"errors": []
}
}
}Do not set Content-Type yourself. When you use -F, curl sets multipart/form-data with the correct boundary automatically.
Upload by ID
If you already have a product ID and environment ID, pass them instead of names. IDs are exact and skip the name lookup.
Choosing where it goes
sbomUpload accepts these target inputs. Pass the product and the environment.
projectGroupName
String
Product, by name
projectGroupId
ID
Product, by ID
projectName
String
Environment, by name (default, development, production)
projectId
ID
Environment, by ID
If you omit the environment, the upload goes to default.
Supported file formats
CycloneDX, JSON and XML
SPDX, JSON and tag-value
After the upload
A new version is created and the platform starts processing it: vulnerability scanning, automation, and policy checks. The SBOM is not fully ready to download until processing finishes.
Check Processing Status shows how to tell when it is done.
List Products and Versions shows the new version and its ID.
Errors
If the upload fails, the reason is in the errors list:
Common causes:
Project group not found
The product name or ID does not exist, or the token cannot see it.
HTTP 401
The token is missing, expired, or wrong.
See Errors for more.
Recording build provenance
When you upload from a CI/CD pipeline, you can attach provenance: the event, commit, build, and repository behind this SBOM. The platform records it with the new version, so you can trace any version back to the build that produced it.
Provenance travels as X- HTTP headers on the upload request. The multipart body and GraphQL mutation are unchanged. Every header is optional and independent, so send the ones you have and omit the rest.
This is what the pylynk CLI does for you automatically. The examples below show how to send the same headers from curl.
Provenance headers
X-CI-Provider
CI system: github_actions, azure_devops, bitbucket_pipelines, or generic_ci.
X-Event-Type
What triggered the build: push, pull_request, or release.
X-Release-Tag
Tag name, for a release or tag build.
X-PR-Number
Pull request number.
X-PR-URL
Pull request URL.
X-PR-Source-Branch
Branch being merged from.
X-PR-Target-Branch
Branch being merged into.
X-PR-Author
User who triggered the build.
X-Build-URL
Link to the CI run.
X-Commit-SHA
Commit hash.
X-Repository-URL
Repository URL.
From GitHub Actions
GitHub Actions exposes most of these as built-in variables.
A tag push has GITHUB_REF like refs/tags/v1.2.3 and is a release. Add -H "X-Release-Tag: ${GITHUB_REF#refs/tags/}". Pull request fields (number, URL, branches) are not in plain variables; they live in the event payload at $GITHUB_EVENT_PATH, which you can read with jq.
From Azure DevOps
The SYSTEM_PULLREQUEST_* variables are only set on pull request builds. On a plain commit, drop the X-PR-* headers.
From Bitbucket Pipelines
BITBUCKET_TAG is set only on tag builds and BITBUCKET_PR_* only on pull request builds. Drop whichever headers are empty.
From any other CI
On any other system, set the headers from whatever variables your CI exposes. Use generic_ci as the provider and fill in what you have.
pylynk detects GitHub Actions, Azure DevOps, and Bitbucket Pipelines and sets all of these headers automatically. If you already run it in your pipeline, you get provenance without any of the above.
Retrying failed uploads
In a CI/CD pipeline, a single upload attempt is not reliable. Network blips, rate limits, and brief server errors all cause failures that succeed on a retry. Retry transient failures with exponential backoff, and fail fast on permanent ones like a bad token.
Which failures to retry
Retrying a bad token wastes time and never succeeds. Retrying a server error often works on the next attempt.
Network error or timeout
Yes
Transient. The next attempt usually connects.
HTTP 429 (rate limited)
Yes
The server is asking you to slow down. Back off and retry.
HTTP 500, 502, 503, 504
Yes
Transient server error.
HTTP 401 (unauthorized)
No
The token is wrong or expired. Fix it.
HTTP 400, 403, 404
No
The request is wrong. Retrying sends the same bad request.
HTTP 200 with sbomUpload.errors
No
The upload was rejected, for example an unknown product. Fix the input.
Between retries, wait longer each time. Doubling the delay (1s, then 2s, then 4s) gives the API room to recover and avoids a tight retry loop. This is exponential backoff.
Option 1: curl's built-in retry
curl has exponential backoff built in. The --retry flag retries transient failures and doubles the wait between attempts on its own.
--retry 5
Retry up to 5 times. curl waits 1s, then 2s, 4s, 8s, doubling each time.
--retry-connrefused
Also retry when the connection is refused.
--fail-with-body
Exit non-zero on an HTTP error, but still print the response body.
Do not add --retry-delay. Setting a fixed delay turns off curl's exponential backoff. Leave it off and the delay doubles automatically.
curl's --retry retries the transient cases for you: timeouts and HTTP 408, 429, 500, 502, 503, and 504. It does not retry 401 or other 4xx errors, which is what you want.
What --retry cannot see is a GraphQL-level rejection. If the API returns HTTP 200 with a non-empty sbomUpload.errors list, curl treats the request as a success. Always check that field yourself:
This option is enough for most pipelines.
Option 2: a retry script
When you want full control, for example to log each attempt or to treat the GraphQL errors field as a hard stop, use an explicit loop.
This is the same strategy the pylynk CLI uses: three retries with a 1s, 2s, 4s backoff, no retry on authentication or client errors.
A note on duplicate versions
Every successful sbomUpload creates a new version. There is no upload ID you can reuse to make a retry idempotent.
This matters in one specific case: the server accepts the upload, but the response is lost on the way back to you (a dropped connection at exactly the wrong moment). Your script sees a failure and retries, and the retry creates a second version of the same SBOM.
This is rare and usually harmless, since the versions are identical. If your pipeline cannot tolerate it, after a retry that followed an unclear failure, list the versions for that product and environment and remove any duplicate.
Add jitter for fleets of pipelines
If many pipelines upload at once and all hit a rate limit together, they will all back off by the same amount and retry at the same moment, hitting the limit again. Add a small random delay (jitter) so the retries spread out:
Last updated