Skip to main content

Outline

Objective

This guide shows you how to send asset and security-finding data from your own systems into SAFE using the API Connector, so that SAFE can use it for exposure prioritization.

Typical sources are:

  • A security data lake such as Databricks, Snowflake or Redshift
  • A CMDB or asset inventory you maintain
  • Exports from security tools, such as SentinelOne, Tenable or ServiceNow

By the end of this guide you will have:

  1. Created API credentials in SAFE and obtained an access token.
  2. Exported your data as compressed NDJSON files.
  3. Requested secure upload URLs and uploaded your files to SAFE.
  4. Confirmed that the data appears on the Assets and Findings pages, labelled with your source name.

Target Users

This document is intended for Engineers and Architects who wish to understand how to post security findings to SAFE. You do not need prior SAFE API experience, but you should be comfortable running curl commands or a short script.

  • SAFE Administrator: Creates the API credentials in SAFE and confirms the data after upload.
  • Security or Data Engineer: Ingests data from the data lake, builds the NDJSON files, and automates the push.

Prerequisites

On the SAFE side

  • SAFE Admin Access
  • SAFE API base URL: The URL you use to sign in, for example https://{your-tenant}.safescore.ai
  • IP address is allowed: If your tenant restricts access by IP, the machine that calls the SAFE APIs must be on the allowed list

On the Source side

  • Read access to data
  • A way to export to NDJSON: A query plus a script, notebook or ETL job that writes one JSON object per line. Any language that can write JSON and gzip files works.
  • A place to run the push: A server, container or scheduled job with curl or an HTTP client. It must reach your SAFE URL over HTTPS and the upload URLs SAFE returns, which are hosted on Amazon S3
  • A secrets manager:  A place to store the API username and password.
  • A scheduler (recommended): Pushes should repeat, typically every 24 hours. Use your existing orchestrator or cron.

Data requirements

For every asset you send, make sure your data lake (source) can provide:

  1. A source name. The name of the tool the data came from, for example Qualys. It is case-sensitive and appears in SAFE as the asset's source, so keep it identical on every push.
  2. At least one identity field. One of cloudResourceId, hostname, fqdn, netbiosName, a MAC address or an IPv4 address. SAFE uses these to match the record to assets it already knows.
  3. A complete picture of the asset for that source. Each push replaces what that source previously reported for the asset: findings, software, patches and tags missing from the new record are marked as resolved. Plan to send the full current list every time, not only changes.
  4. Open vulnerabilities only. A vulnerability left out of the next push is closed for that source.
  5. Everything else (hardware, cloud details, owners, software, patches, compliance results) is optional. Send what you have; more context gives SAFE better prioritization.

Limitations

Limit Value
Files per upload URL request 100
Maximum file size (compressed) 250 MB
Access token lifetime 60 minutes
Upload URL lifetime Just under 1 hour (see expiresAt in the response)
Upload URLs per tenant 1,000 per minute, shared by every client of your tenant
Size of one line (one asset record, uncompressed) 128 MB
Findings per asset 20,000. An asset with more in one record is not imported.

How it works

You export your data as compressed NDJSON files, ask SAFE for upload URLs, and upload the files over HTTPS. SAFE then matches each asset to the assets it already knows, merges the records, and shows the data on the Assets and Findings pages, labelled with the source name you sent.

Step What you do What SAFE does
1. Authenticate Call POST /api/v3/auth with your API credentials. Returns an access token, valid for 60 minutes.
2. Prepare files Write your asset records as NDJSON, gzip each file, and note each file's size in bytes. Nothing yet.
3. Request upload URLs Call POST /api/v3/integrations/file-ingest/uploads with the size of each file. Returns one secure, time-limited upload URL per file.
4. Upload Upload each file to its URL with an HTTP PUT. Stores the file in your SAFE storage. A 200 OK confirms SAFE has it.
5. Review Open the Assets and Findings pages. Processes the files in the background and shows the data.

Step-by-step procedure

Authenticate

Create API credentials

Skip this if you already have a credential for this feed.

  1. In SAFE, go to Settings > API Credentials and select +.
  2. Enter a name (for example data-lake-uploader), choose an expiry and select Add.
  3. Copy the API Username and Password that SAFE shows and store them in your secrets manager. The password is shown only at creation. The new credential then appears under Settings > API Credentials.

Get an access token

Exchange the credentials for an access token with HTTP Basic authentication:

curl -X POST "https://{your-tenant}.safescore.ai/api/v3/auth" \
  -u "$SAFE_API_USERNAME:$SAFE_API_PASSWORD"

The response contains accessToken, save it.

  • A token is valid for 60 minutes. Reuse it for every call in that time instead of authenticating before each call.
  • If a SAFE API call returns 401, the token has expired or is invalid. Authenticate again and retry the call once.

Prepare your files

File format

  • NDJSON (newline-delimited JSON), UTF-8. Write one asset per line as {"signals":[ {one signal} ]}. Each line is a complete JSON object and lines are separated by \n.
  • Do not wrap the lines in an array. Do not put more than one signal in a line's signals array: such a line is skipped.
  • gzip-compress every file. The upload URL is signed for application/gzip. Any local file name works; *.ndjson.gz is a useful convention.
  • Send each asset once per file, and never split one asset across lines or files.
  • Put many assets in each file. Fewer, larger files are better than many small ones. Never send one asset per file.

Example

You only need the fields below to get started. In your file this record is written on one line; it is formatted here for reading.

{
  "signals": [
    {
      "version": "2.0.0",
      "id": "6f1d2c3b-8a4e-4f5a-9b7c-0d1e2f3a4b5c",
      "type": "default",
      "entity": {
        "source": "Qualys",
        "lastSeen": "2026-10-16T18:00:00Z",
        "entityAttributes": {
          "hostname": "WEB-PROD-01",
          "networkInterface": [
            { "ipv4": "10.20.30.40", "macAddress": "0a:1b:2c:3d:4e:5f", "isPrimary": true }
          ]
        }
      },
      "securityContexts": [
        {
          "type": "va",
          "name": "CVE-2024-6387",
          "severity": "high",
          "status": { "complianceStatus": "fail" }
        }
      ]
    }
  ]
}

Record structure

Each line is one signal. version, id, type and entity.source are required, plus at least one identity field. Every other section is optional: send only the sections and fields you have.

Section What it holds Required inside each item
version, id, type The envelope: always "2.0.0", your UUID for the line, always "default" All three
entity source, sourceAssetId, firstSeen, lastSeen and entityAttributes source
entity.entityAttributes The device: names, network interfaces, type, tags, platform, hardware and OS, cloud details, location, owners At least one identity field (see below)
securityContexts[] Findings: vulnerabilities (type: "va") and configuration control results (type: "ca") type, name, status.complianceStatus; a ca also needs controlId and policyId or policyName
softwares[] Installed software and agents (not the operating system) name
patches[] Patches (KB numbers) patchNumber, status
compensatoryControls[] Security-tool configuration evidence (EDR …), one block per type. Send it in the security tool's own files, for example under source SentinelOne type, and at least one control with controlId, controlName and value

Every field, with its format and meaning, is listed in Field reference below.

Optional sections add context: hardware and OS, cloud details, owners, softwares[], patches[] and compensatoryControls[]. Send only what you have. Keys SAFE does not recognise are ignored.

Asset identity: how SAFE recognises a device

Every signal needs at least one identity field: cloudResourceId, hostname / fqdn / netbiosName, networkInterface[].macAddress or networkInterface[].ipv4. SAFE treats two records as the same asset at the first rule that matches:

  1. Cloud resource ID is the same (cloudResourceId). Always send it for cloud resources.

  2. Host name is the same (hostname, fqdn or netbiosName; not case-sensitive; WEB01 matches WEB01.corp.example.com), and the serial numbers do not differ.

  3. MAC address is the same, when one of the two records has no host name. Only the primary interface's IP and MAC (isPrimary, else the first valid one) are used for matching; other interfaces are shown, not matched.

  4. IP address is the same, when one of the two records has no host name. The weakest rule: IPs change and public IPs can be shared (NAT), so send private addresses where you can.

Host name and FQDN together. When fqdn contains a dot, SAFE keeps hostname and netbiosName only if they are the same as fqdn; otherwise it keeps the FQDN alone. Matching is not affected (a short name still matches its FQDN), but the short names are not shown. If you have an FQDN, send it in fqdn; send hostname alone only when there is no FQDN.

Skipped records. A record with no usable identity is skipped. Placeholders do not count: 0.0.0.0, 127.0.0.1, 169.254.x.x, IPv6 addresses, 00:00:00:00:00:00, and serials such as N/A. A virtual-machine MAC address (VMware, VirtualBox, Docker and similar) is stored but never used to match. A generic host name such as LINUX, UBUNTU or IPHONE needs a MAC address with it, or the whole record is skipped.

The full-snapshot rule

For each asset and source in a push, SAFE treats the record as the complete, current picture:

  • Items in the new signal are added or updated.

  • Items SAFE had from that source for that asset, but that are missing from the new signal, are marked as gone (resolved, not deleted). This applies to findings, software, patches, asset tags, location, and compensatory controls (when compensatory-control assessment is enabled for your tenant).

  • Assets that are not in the push are not changed.

  • Data from other sources on the same asset is not changed.

Always send the full list of findings, software, patches and compensatory controls for an asset, not only what changed since the last push. Send open vulnerabilities only: a vulnerability missing from the source's next signal for that asset is closed.

Source

  • entity.source names the tool the data came from, for example Qualys. It is required in every line. A file may contain lines from several sources; each line is processed under its own source.

  • Any name is accepted, exactly as you send it. Names are case-sensitive: Qualys and qualys are two different sources. Keep them consistent between pushes.

  • A name may be up to 255 characters, must not be only digits, and must not be manual.

  • If two tools report the same asset, send each tool's data under its own source name, in its own files or as its own group of rows. SAFE merges them into one asset.

Values

Kind Format
Dates and times ISO 8601 with a time zone, for example 2026-09-30T18:00:00Z or 2026-09-30T23:30:00+05:30. Always include the zone: a time without one, or a date alone, is read as UTC (a date as midnight), which shifts a local time.
Numbers Integers, for example "cpuCores": 40. A numeric string or a decimal is also accepted; decimals are truncated.
JSON types Send each value in the type the field reference gives. A number or true/false in a text field is kept as text; an object or array in a single-value field drops only that field. Identity fields (hostname, fqdn, netbiosName, ipv4, macAddress, cloudResourceId), finding names, envelope fields, severity and timestamps must be JSON strings, or the field is dropped. A wrong structure, such as hardware or status sent as a string, skips the whole line.
Finding type va (vulnerability) \
Finding status status.complianceStatus: fail \
Finding severity severity (a plain string, for example "severity": "high"): critical \
Software type application \
Patch status INSTALLED \
Location An array with one full country name, for example ["United States"] (a single string is also accepted). Use the same spelling every time.
Owners One string per owner field; a list is accepted and joined with ", ".
Keys camelCase, exactly as in the field reference. Unknown keys are ignored and the line is still processed. (Unknown fields in the upload URL request are rejected with 400.)

Field reference

Every key is optional unless the Required column says otherwise. Keys not listed here are ignored.

Envelope and entity

Field Required Format / allowed values Meaning and what SAFE does
version Yes "2.0.0" exactly Contract version of the record. Any other value skips the whole line.
id Yes UUID (any version) Your own unique id for this line. Must be a valid UUID; a line with an invalid id is skipped. Not stored: SAFE assigns its own asset id.
type Yes "default" exactly Record type. Any other value skips the line.
entity.source Yes String, 1–255 bytes (spaces trimmed); not digits only; not manual in any case. Case-sensitive. Name of the tool or system that reported the asset, for example Qualys. Shown as the asset's source. A finding this source reported before and is missing from its newest signal for the asset is closed.
entity.sourceAssetId No String The asset's id inside the source tool. Kept by SAFE for traceability; not shown.
entity.firstSeen No ISO 8601 timestamp When the source first saw the asset. Shown as the source's first-seen date; set when SAFE first records the asset for this source and not moved by later files.
entity.lastSeen No ISO 8601 timestamp When the source last saw the asset. Shown as the source's last-seen date; updated on every push.

Identity, network, location and owners

Field Required Format / allowed values Meaning and what SAFE does
hostname One of the identity fields String (must be a JSON string); shown uppercased Short host name. Identity key; shown in Network Interfaces. See Host name and FQDN together. A name that only wraps an IP (for example ip-10-1-2-3) is not used as a host name; SAFE uses the IP instead.
fqdn One of the identity fields String (must be a JSON string), for example web-prod-01.corp.example.com; shown uppercased Fully qualified DNS name. Identity key; shown in Network Interfaces; used for the asset name when there is no cloud name.
netbiosName One of the identity fields String (must be a JSON string), up to 15 characters; shown uppercased Windows NetBIOS name. Identity key; shown in Network Interfaces.
networkInterface[].ipv4 One of the identity fields Dotted IPv4 string. 0.0.0.0, 127.0.0.1 and 169.254.x.x are dropped; IPv6 is not accepted. Public addresses are accepted, but send private addresses where you can: a shared public IP can match unrelated devices. IPv4 address of one interface. Identity key; shown in Network Interfaces (newest first).
networkInterface[].macAddress One of the identity fields String: aa:bb:cc:dd:ee:ff, aa-bb-…, aabb.ccdd.eeff or 12 hex digits; stored lowercase. 00:00:00:00:00:00 is dropped. MAC address of the same interface. Identity key; shown in Network Interfaces.
networkInterface[].isPrimary No true \ false ("yes"/"no" and 1/0 are also accepted). The first true wins; without one, the first valid interface is primary.
sourceAssetType No String Asset type as the source names it, for example Server. Shown as sent; SAFE derives its own asset category separately.
sourceAssetSubtype No String Asset subtype as the source names it, for example Web Server. Shown as sent.
assetTags[].value No Non-empty string; tags made only of special characters are dropped A free-form tag. Shown as an asset tag; filterable.
platform No windows \ linux \
location No Array with one full country name, for example ["United States"] (a single string is also accepted). Only the first value is used; shown uppercased. Use the same spelling every time. Country the asset is in. Shown as the asset's location; filterable.
businessOwner, itOwner, vmOwner, deviceOwner No One string each; a list is accepted and joined with ", " Owners of the asset (name or email). Shown on the asset. When several sources report an owner, SAFE shows one source's value.

Hardware and operating system

Field Required Format / allowed values Meaning and what SAFE does
osName, osVersion, osVendor No String Operating system name, version and publisher. Shown under Hardware and as the OS software row. SAFE normalises them (for example "Microsoft Windows Server 2019 Datacenter" may show as "Windows Server 2019").
osArch No String, for example x64 OS architecture. Shown under Hardware.
osReleaseInstallDate No ISO 8601 timestamp When the current OS release was installed. Shown under Hardware.
osInstallTime No ISO 8601 timestamp When the OS was first installed. Shown as the install date of the OS software row.
osKernel No String, for example 10.0.17763.5329 Kernel or full build number. Not shown; SAFE uses it to work out the Windows build when osVersion has none, which drives patch state and CVE closure.
serialNumber No String; placeholders such as N/A or "To be filled by O.E.M." are ignored Hardware serial number. Shown under Hardware. Two different serial numbers are never merged. A serial number cannot be an asset's only identity field; sent with an IP or MAC address, a matching serial can link two records.
systemManufacturer, systemModel No String Hardware maker and model. Shown under Hardware; used to recognise virtual machines and device types.
cpuModelName No String CPU model. Shown under Hardware.
cpuCores, totalPhysicalMemMB No Integer (a numeric string or a decimal is accepted; decimals are truncated) Number of CPU cores and RAM in megabytes. Shown under Hardware.
biosVendor, biosVersion No String BIOS vendor and version. Shown under Hardware.
biosReleaseDate No ISO 8601 timestamp BIOS release date. Shown under Hardware.

Send the operating system in hardware, not in softwares. Send agents as softwares.

Cloud

Field Required Format / allowed values Meaning and what SAFE does
cloudResourceId One of the identity fields String (must be a JSON string): AWS ARN, Azure resource ID or GCP URI, at most 512 characters; stored lowercase. A placeholder such as -, 0, n/a, none or null counts as no id. The cloud's unique id for the resource. Strongest identity key; marks the asset as a cloud asset. Without a valid id, the other cloud fields are ignored.
cloudServiceProvider No AWS \ Azure \
cloudRegion No String, for example us-east-1 Cloud region. Shown under Cloud.
cloudAccountId No String (a number is accepted) Cloud account, subscription or project id. Shown under Cloud.
cloudNativeAssetType No String, for example AWS::EC2::Instance The provider's own resource type. Shown under Cloud; drives cloud categorisation.
cloudAssetType No String, for example ec2-instances Resource type in plain words. Shown under Cloud.
cloudInstanceId No String, for example i-0abc123 Instance id. Shown under Cloud.
cloudAssetName No String Resource name in the cloud. Shown under Cloud; becomes the asset's display name.
cloudVpcId, cloudSubnetId, cloudResourceGroupName No String VPC, subnet and resource group. Shown under Cloud.
containerInfo[].containerName Yes, per item Non-empty string Name of a container running on the resource. The number of distinct names is the container count.
containerInfo[].containerCount No Integer (a numeric string is accepted) Running instances of that container. Shown with the image attributes.
imageAttributes.registry, namespace, name, imageTags, digest No String The container image the resource runs. Shown under image attributes.

Vulnerabilities

Field Required Format / allowed values Meaning and what SAFE does
type Yes "va" Marks the finding as a vulnerability.
name Yes For a CVE, exactly one bare CVE id, for example CVE-2023-44487 (one finding per CVE). Otherwise the title. The vulnerability. For a known CVE, SAFE adds the NVD description, CVSS and EPSS.
status.complianceStatus Yes fail \ pass
severity No Plain string: critical \ high \
sourceFindingType, sourceFindingSubtype No String Your source's own category and sub-category. Shown as sent.
description.technical No Text Technical description. Shown; for a known CVE the NVD text replaces it.
openSinceInSource, lastAssessed No ISO 8601 timestamp When the source first found it and last checked it. Shown.
remediation.description No Text How to fix it. Shown as remediation.
threatTags No Array of strings Threat tags from the source. Used for CVEs. Recognised tags (exact text, case-sensitive): CISA Known Exploit (marks the CVE as CISA KEV), Known Exploit, Ransomware Linked, Malware Linked, Exploit Kit Available. Other tags are kept but do not affect scoring.
affectedSoftware.name, version, vendorName, type, installPath, installDate name, when affectedSoftware is sent As in softwares[]; do not use an OS type. The software the vulnerability is in. Use the same values as the matching softwares[] entry so SAFE links the two; any difference in name, version or install path creates a second software row.

Configuration controls

Field Required Format / allowed values Meaning and what SAFE does
type Yes "ca" Marks the finding as a configuration control result.
name Yes Text The control's statement. Shown; replaced by the CIS title when SAFE recognises the control.
controlId Yes String, for example 18.9.47.4.1 The control's id in its benchmark. Shown as the control id and used to look the control up in SAFE's CIS library.
policyId, policyName At least one String The policy the control belongs to. Controls with the same policyId and policyName are grouped into one policy. For CIS policies SAFE generates the policy id from the name.
complianceFramework No String, for example CIS Framework. Shown.
status.complianceStatus Yes fail \ pass
severity No Plain string: critical \ high \
sourceFindingType, sourceFindingSubtype No String Your source's own category and sub-category. Shown as sent.
description.technical, remediation.description No Text Why the control matters and how to pass it. Shown; the CIS text replaces them when SAFE recognises the control.
openSinceInSource, lastAssessed No ISO 8601 timestamp When the source first and last evaluated the control. Shown.

Software, patches and compensatory controls

Field Required Format / allowed values Meaning and what SAFE does
softwares[].name Yes Non-empty string Product name. Shown in the software inventory; SAFE may normalise it.
softwares[].vendorName, version, installPath, installDate No String; installDate is an ISO 8601 timestamp Vendor, version, install location and install time. Shown. Name, version and install path identify the software row.
softwares[].type No application \ cpe
softwares[].kernel No String Kernel the software is built for. Not shown; improves SAFE's product matching.
patches[].patchNumber Yes String, for example KB5034439 (a number is kept as text) Patch or KB id. Not listed in the UI; SAFE uses patches to compute software patch state and to close the CVEs a patch fixes.
patches[].status Yes INSTALLED \ PENDING \
patches[].installDate Recommended for an INSTALLED patch ISO 8601 timestamp When it was installed. Without a date, an installed patch is kept but cannot close any CVE.
compensatoryControls[].type Yes String, for example EDR; one block per type Kind of protection.
compensatoryControls[].sourceTool, siteId, siteName, groupId, groupName, agentVersion, agentLastActive No String; ids may be numbers; agentLastActive is an ISO 8601 timestamp The product enforcing the controls, its site and group, and the agent's version and last check-in. Stored with the asset's compensatory control assessment.
compensatoryControls[].controls[].controlId, controlName, value Yes Text; a number or true/false is kept as text Each configured setting. Used to assess whether the control reduces the risk of a finding (SentinelOne EDR today).

Notes:

Each upload URL is signed for one exact file size. So always:

  1. Write the NDJSON and gzip it to a file.

  2. Read the file's size in bytes from disk (for example os.path.getsize() or stat).

  3. Do not re-compress, re-encode or otherwise change a file after measuring it. gzip output changes with the tool, level and timestamp, so a re-compressed file has a different size and S3 refuses the upload.

Request upload URLs

Ask for one upload URL per file, listing the files' sizes in the order you will upload them:

curl -X POST "https://{your-tenant}.safescore.ai/api/v3/integrations/file-ingest/uploads" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"files": [{"sizeBytes": 152340}, {"sizeBytes": 98211}]}'
Rule Limit
Body Required. Exactly {"files": [{"sizeBytes": N}, ...]}; any other field is rejected (400).
Files per call 1 to 100
sizeBytes Whole number, the exact size in bytes of the gzipped file, from 1 up to the maximum file size
Upload URLs per tenant Limited per minute; see Limits

A successful call returns 201:

{

"success": true,

"data": {

"expiresAt": "2026-10-13T12:59:00Z",

"uploads": [

{

"uploadId": "0b6e…",

"url": "https://….s3.….amazonaws.com/…",

"method": "PUT",

"headers": {

"If-None-Match": "*",

"Content-Type": "application/gzip",

"Content-Length": "152340"

}

}

]
}
}
  • uploads is in the same order as files: uploads[0] is for the first file you listed.

  • Each upload has an uploadId. Log it with your file name: SAFE support uses it to trace a file. Do not send it back.

  • expiresAt is when the URLs stop working, currently just under one hour after the call. Use this value rather than assuming a fixed duration.

  • Request URLs only when the files are ready, and only for as many files as you can upload before expiresAt.

Upload the files

Upload each file with an HTTP PUT to its url, sending exactly the headers listed for it and the file as the raw request body:

curl -X PUT "$UPLOAD_URL" \
  -H "If-None-Match: *" \
  -H "Content-Type: application/gzip" \
  --data-binary @assets-part-1.ndjson.gz
S3 response Meaning What to do
200 OK SAFE has the file. Done. Processing starts automatically.
412 Precondition Failed A file was already uploaded to this URL. Nothing; the first upload stands. A file cannot be overwritten.
403 SignatureDoesNotMatch The body size differs from sizeBytes, a listed header is missing or different, or the upload used chunked transfer. Fix the request. If the file changed after you measured it, request a new URL for it.
403 AccessDenied with message Request has expired The URL is past expiresAt. Request a new URL for that file and upload again.
403 AccessDenied (any other message) SAFE storage refused the upload. Do not retry. Contact SAFE support with the uploadId and upload time (UTC).

An upload URL is a secret. Anyone who has it can upload that one file, once, until it expires, without signing in. It cannot read, list, delete or overwrite anything. Do not log, store or share upload URLs.

What happens after you upload

  • A 200 on the upload is your confirmation. There is no separate status API: SAFE validates and processes the file in the background.

  • SAFE matches every asset to the assets it already knows (see Asset identity in Step 2) and applies the full-snapshot rule per source.

  • Lines that cannot be read, or signals SAFE cannot identify, are skipped; the rest of the file is still processed.

  • The data appears on the Assets and Findings pages after processing. In the Asset Source column, data lake sources show the data lake icon; hover over it to see the source name. Use the Asset Source filter to show one source.

Situation What you see in SAFE
Qualys and Tenable both report server WEB-01 (each in its own files) One asset, WEB-01, listing both sources.
Both report CVE-2023-44487 on it The vulnerability is kept once per source on that one asset.
The same software appears with a new version The new version is added; the old one is marked as gone when it stops being sent.
A vulnerability is left out of Qualys's next signal for WEB-01 The vulnerability is closed for Qualys on WEB-01.

Automate the push

A one-off upload loads SAFE once. To keep it current, run the same flow on a schedule.

  • Start with one full push, then push regularly. Send every asset once to load SAFE, then push on a schedule, typically once every 24 hours. Each asset you send must still be its full picture.
  • Batch your assets. Put many assets in each file and up to 100 files in each push.
  • Do not upload the same asset and source twice at once. Processing order for two files that contain the same asset and source is not guaranteed.

APIs used

The integration uses two SAFE APIs and one upload to SAFE storage. Replace https://{your-tenant}.safescore.ai with your SAFE URL.

# API Purpose Authentication
1 POST /api/v3/auth Exchange API credentials for an access token HTTP Basic: API username and password
2 POST /api/v3/integrations/file-ingest/uploads Request one pre-signed upload URL per file Authorization: Bearer {accesstoken}
3 PUT {upload url} (returned by API 2, hosted on Amazon S3) Upload one gzipped NDJSON file None. The URL is pre-signed; send exactly the returned headers.

Best practices

  • Start with one full push, then push regularly. Send every asset once to load SAFE, then push changed assets on a schedule, typically once every 24 hours. Each asset you send must still be its full picture.

  • Keep each source together. Send each source's assets in as few, full files as possible, and within a file keep each source's rows together rather than mixed. One file per tool is the simplest way.

  • Batch assets. Put many assets in each file and up to 100 files in each push. Avoid one asset per file or one push per asset. At the current rate limit a tenant can request 1,000 upload URLs a minute, for example ten pushes of 100 files.

  • Do not upload the same asset twice at once. Do not upload two files that contain the same asset and source at the same time: the order in which they are processed is not guaranteed.

  • Send identity well. Always send cloudResourceId for cloud resources and macAddress with generic host names. Keep host names stable between pushes; renaming a device can create a second asset.

  • Keep source names and finding text stable. Changing a source name creates a new source; rewording the name of a non-CVE finding creates a new finding.

  • Validate before uploading. Check that every line is valid JSON, has the envelope fields, entity.source and an identity field, and uses the JSON types in the field reference.

  • Handle errors. Re-authenticate once on 401; on 429 wait for Retry-After; on 5xx retry with exponential backoff; do not retry other 4xx errors unchanged.

  • Protect secrets. Keep API credentials in a secrets manager and never log access tokens or upload URLs. Log the uploadId and file name instead.

Errors and troubleshooting

Upload URL API : POST /api/v3/integrations/file-ingest/uploads

Status Cause Fix
400 The body is not {"files": [{"sizeBytes": N}, ...]}, there are 0 or more than 100 files, or a sizeBytes is out of range. The message names the file, for example files[1].sizeBytes must be between 1 and 262144000. Correct the body. Split large files.
401 Expired or invalid access token Authenticate again (Step 1) and retry.
403 file ingest is not enabled for this tenant The API Connector is not enabled for your tenant Contact your SAFE representative.
403 (other) No access token was sent; the user does not have permission to manage integrations; or the calling IP address is not in your tenant's allowed IP list Send the token; use API credentials of a user with that permission; or add the IP address to your allowed list.
429 upload URL limit reached for this tenant; retry after N seconds: your tenant has used its upload URLs for the current minute. A 429 without Retry-After is a general API request limit, separate from the upload URL limit. Wait the number of seconds in Retry-After (back off exponentially if it is absent), then retry. A request for fewer files may succeed sooner.
5xx Temporary SAFE error; no URLs were returned Retry with exponential backoff.

Did this article help?

Let us know by leaving a star rating at the top of this article.