Skip to content
v2 ➔ v3 Migration Guide

Migrating from the cysmo® API v2 to v3

Welcome to the migration guide for the cysmo® API. This guide walks you through everything that changed between v2 and v3, so you know exactly what to adjust in your integration.

v3 is not a compatible extension of v2. Endpoints moved, objects were restructured, and some fields changed their type. We recommend reading the Getting Started guide as well as the API Specification alongside this document for a deeper understanding.

What Changed at a Glance

Companies are organised in portfolios. When creating a company, you can now sort them into portfolios. Portfolios carry user permissions and optionally a default radar, and rule sets.

The company object is nested. What used to be one flat object is now split into data (name, address, legal form, …), scanOptions (domains and further scan parameters) and latestRating.

Reports are now ratings. Everything under /v2/reports moved to /v3/ratings, and a company can expose its full rating history. The term “reports” is now solely used for downloadable representations of a rating, like PDFs.

PUT Updates instead of PATCH We removed all convoluted PATCH logic and replaced them with simple PUT logic: You send the whole object in its new state and we replace it entirely. For commonly updated fields (like the customer/interested status or risk question answers) we introduced sub-paths to make things easier.

Turnover and employee count are absolute numbers. The bucket enums are gone.

PDF generation is asynchronous. You request a report, then poll a download endpoint until the file is ready.

Errors follow RFC 9457. All error responses use the same ProblemDetail object.

Authentication

v3 still uses the OAuth2 client credentials flow, but the token URL changed. While the v2 API served an optional redirect URL, we now recommend sending your request to the login server directly.

v2:  https://api.cysmo.de/oauth/token
v3:  https://login.cysmo.de/realms/cysmo/protocol/openid-connect/token

Object Changes

Company

In v2 a company was a single flat object. In v3 the fields are grouped by purpose: data holds general data that has no influence on the technical rating, scanOptions controls what is scanned and how, and latestRating gives you an overview of the most recent results.

GET
  /v2/companies/{companyId}
API v2
// Response
{
  "id": "e1c7...",
  "name": "My first cysmo company",
  "ratingModel": "SIMPLE",
  "domains": ["ppi.de"],
  "nets": [],
  "legalForm": "legalStructure.STOCK_CORP",
  "numberOfEmployees": "numberOfEmployees.FROM5000",
  "turnover": "turnover.FROM50",
  "industry": "A1",
  "city": "Hamburg",
  "zipCode": "20459",
  "country": "DE",
  "status": "status.CUSTOMER",
  "insuranceId": "CRM-4711",
  "clientId": "2a9b...",
  "needsAdditionalInformation": false,
  "modificationDate": "2026-08-01T10:00:00Z",
  "reports": []
}
// Response
{
  "id": "e1c7...",
  "portfolioId": "2a9b...",
  "data": {
    "name": "My first cysmo company",
    "legalForm": "GENERIC__STOCK_CORPORATION",
    "numberOfEmployees": 5000,
    "turnover": 72000000,
    "currency": "EUR",
    "industry": "A1",
    "city": "Hamburg",
    "zipCode": "20459",
    "country": "DE",
    "customerStatus": "CUSTOMER",
    "externalReference": "CRM-4711"
  },
  "scanOptions": {
    "ratingModel": "SIMPLE",
    "infrastructureSize": "NORMAL",
    "scanTargetDomains": ["ppi.de"],
    "scanTargetNets": [],
    "associatedDomains": [],
    "associatedNets": [],
    "ignoredDomains": [],
    "ignoredNets": []
  },
  "dataComplete": true,
  "riskQuestionsComplete": false,
  "outdated": { "financialLoss": false },
  "modifiedOn": "2026-08-01T10:00:00Z",
  "latestRating": { }
}
v2 v3 Notes
id id Unchanged.
name data.name Moved.
legalForm data.legalForm Completely new enum with international and generic variants. See below.
numberOfEmployees data.numberOfEmployees Now an integer.
turnover data.turnover Now an integer.
industry data.industry Moved. NACE codes are unchanged.
city data.city Moved.
zipCode data.zipCode Moved. Validated against ^[a-zA-Z0-9\- ]{3,10}$.
country data.country Moved. Validated as a two-letter code.
status data.customerStatus Renamed. The status. prefix is gone.
insuranceId data.externalReference Renamed.
— data.currency New, read-only. Always EUR for now.
domains scanOptions.scanTargetDomains Renamed.
nets scanOptions.scanTargetNets Renamed.
ratingModel scanOptions.ratingModel Moved. SIMPLE and ASSOCIATED are unchanged.
— scanOptions.infrastructureSize New. NORMAL or LARGE.
— scanOptions.associatedDomains / associatedNets New, read-only.
— scanOptions.ignoredDomains / ignoredNets New.
— portfolioId New. Currently only writable on creation. We are working on a way to move companies.
modificationDate modifiedOn Renamed.
modifiedBy modifiedBy Unchanged.
createdBy createdBy Unchanged.
needsAdditionalInformation dataComplete and riskQuestionsComplete Replaced by two fields with inverted meaning.
reports latestRating Only the latest rating is embedded. Use GET /companies/{companyId}/ratings for the history.
— outdated New, read-only. Flags stale financial loss and rule evaluation results.
answers riskQuestionAnswers Renamed.

Note

numberOfEmployees and turnover no longer use buckets. Provide the actual headcount and the annual turnover in full notation without decimals, so 72 million euro becomes 72000000.

The new legalForm enum differentiates between international (DE__) and generic (GENERIC__) legal forms. We are planning to expand the international variants in the future.

Rating

The v2 Report object became the v3 Rating. The term Report now only defines a PDF representation of a Rating. These meanings were mixed in our old terminology.

Besides the renamed fields, the deprecated convenience booleans were removed. Derive the equivalent from state.

Also the smaller rating overview in company.latestRating and
GET /v3/companies/{companyId}/ratings is now more coherent with the complete rating object returned by GET /v3/ratings/{ratingId}.

GET
  /v2/reports/{reportId}
API v2
{
  "id": "9f31...",
  "state": "FINISHED",
  "finished": true,
  "failed": false,
  "running": false,
  "timedOut": false,
  "overflow": false,
  "creationDate": "2026-08-01T10:00:00Z",
  "endDate": "2026-08-01T10:01:30Z",
  "value": 80,
  "ratingTerm": "B",
  "progress": 100
}
{
  "id": "9f31...",
  "state": "FINISHED",
  "companyId": "e1c7...",
  "cause": "RE_RATING",
  "createdOn": "2026-08-01T10:00:00Z",
  "finishedOn": "2026-08-01T10:01:30Z",
  "value": 0.8,
  "grade": "B",
  "progress": 1
}
v2 v3 Notes
id id Unchanged.
state state Three values were renamed, see below.
finished, failed, running, timedOut, overflow — Removed. Evaluate state instead.
creationDate createdOn Renamed.
endDate finishedOn Renamed.
ratingTerm grade Renamed. Values A to F are unchanged.
progress progress Now on a scale from 0 to 1.
valueTech value Removed extra values for Business and Sales mode. Now on a scale from 0 to 1.
companyId companyId Unchanged.
partialRatings partialRatings Unchanged in purpose, some renamings. See spec for details.
cause cause Unchanged.
underwritingClassification ruleEvaluations.underwriting Moved, to have the option of multiple rule sets in the future.
answers riskQuestionAnswers Renamed.
— scanOptions New. The scan options the rating was created with.

These rating states were renamed:

v2 v3
SCORE_TIMEOUT FINISHED_WITH_SCORE_TIMEOUT
ERROR FAILED
OVERFLOW INFRASTRUCTURE_TOO_LARGE

All other states keep their names. Ratings in the states FINISHED and FINISHED_WITH_SCORE_TIMEOUT deliver data. The terminal states are FAILED, FINISHED, FINISHED_WITH_SCORE_TIMEOUT, INFRASTRUCTURE_TOO_LARGE, TIMEOUT, NOT_IN_RATING_ENGINE and INTERNAL_ERROR.

Tip

If a rating ends in INFRASTRUCTURE_TOO_LARGE, mark the company’s infrastructureSize as LARGE. This triggers an automatic re-rating optimized for larger infrastructures. A simple re-rating will not fix the issue. Please note that LARGE ratings can take longer and might have a longer queue time.

Radar Configuration

The radar object got cleaned up, removing the duplicate nextRating, nextRatingDate and nextRatingTime fields and the new field names should bring more clarity. Also the timezone now defaults to the more sensible international option UTC instead of Europe/Berlin. You can of course still set your own.

GET
  /v2/companies/{companyId}/radar
API v2
// Response
{
  "id": "5b90...",
  "nextRatingDate": "2026-09-24",
  "nextRatingTime": "18:00",
  "nextRatingZoneId": "Europe/Berlin",
  "endDate": "2026-12-31T00:00:00Z",
  "interval": 1,
  "intervalUnit": "MONTHS",
  "lastModified": "2026-08-01T10:00:00Z"
}
{
  "nextExecutionDate": "2026-09-24",
  "executionTime": "18:00",
  "timezone": "Europe/Berlin",
  "endDate": "2026-12-31",
  "interval": 1,
  "intervalUnit": "MONTHS",
  "modifiedOn": "2026-08-01T10:00:00Z"
}
v2 v3 Notes
id — Removed.
nextRating — Removed. It was already deprecated and readOnly in v2.
nextRatingDate nextExecutionDate Renamed. Required.
nextRatingTime executionTime Renamed. Still defaults to 18:00.
nextRatingZoneId timezone Renamed. The default changed from Europe/Berlin to UTC.
endDate endDate Now a plain date instead of a date-time. Uses end of day based on the provided timezone.
interval interval Required, with a minimum frequency of one week.
intervalUnit intervalUnit Required. Values are unchanged.
lastModified modifiedOn Renamed.
modifiedBy modifiedBy Unchanged.

v2 restricted MONTHS to the values 3 and 6 and YEARS to 1. v3 provides more freedom and allows any interval > 1 week. Set timezone explicitly so your ratings are scheduled during your off-hours.

Underwriting Result

To accommodate more than one possible rule set in the future, the Underwriting Results have moved from GET /v2/underwriting/{ratingId} to
GET /v3/ratings/{ratingId}/rule-evaluations/underwriting?language=en and the object got cleaned up quite a bit.

GET
  /v2/underwriting/{ratingId}
API v2
{
  "classification": "RED",
  "rules": [
    {
      "classification": "RED",
      "rule": "",
      "name": "OpenPortsNoBadFindings",
      "score": "attack_resilience.open_ports",
      "triggered": true,
      "errors": [],
      "companyDataRequiredExpand": false,
      "riskQuestionAnswersRequired": false,
      "labels": {
        "en": {
          "name": "Critical finding in \"Open Ports\"",
          "description": "A critical finding cannot occur in the score \"Open Ports\".",
          "question": "Can the ports concerned be made inaccessible from the outside?"
        },
        "it": {
          // ...
        }
      }
    }
  ]
}
{
  "overall": "RED",
  "rules": [
    {
      "result": "GREEN",
      "key": "OpenPortsCriticalOrWorse",
      "legacyName": "OpenPortsNoBadFindings",
      "score": "attack_resilience.open_ports",
      "labels": {
        "description": "A critical finding cannot occur in the score \"Open Ports\".",
        "question": "Can the ports concerned be made inaccessible from the outside?"
      }
    }
  ]
}
v2 v3 Notes
classification overall Renamed. The values GREEN, RED, UNKNOWN and ERROR are unchanged.
rules.[].classification rules.[].result Was always RED in v2. Now is the actual indicator in v3.
rules.[].triggered — Removed. Check result == 'RED' instead.
rules.[].name rules.[].key Renamed. Now a fixed technical key instead of a customizable name.
rules.[].name rules.[].legacyName Old value of the v2 name. For your reference during migration. Will be removed when v2 goes EOL.
rules.[].score rules.[].score Unchanged.
rules.[].labels rules.[].labels Removed the translatable name label, CSV Exports will use the static key in the future instead.
Only returns the labels for the language requested in the query parameter

Tip

Querying any ratings underwriting result returns all rules of that company’s portfolio. Use that list together with the field legacyName to migrate your rules to the new key identifier.

Errors

Errors in API v3 are RFC 9457 compliant ProblemDetail objects.

You can match on the type name (after the #) to catch and handle different error cases gracefully in your application. title and detail are meant for human reading, rather than automatic parsing.

If you need to contact cysmo® support in regards to an error message, make sure to include the correlationId, so we can find your case in our logs.

See Possible Error Responses for more details.

Note

Although most errors are already migrated to the new scheme, some errors are still non-compliant. This mostly concerns error messages from our firewall or load balancers rather than application errors.

ERROR
  Old cysmo Error Object
API v2
{
  "error": "Internal Server Error",
  "errorDescription": "An unexpected error occurred."
}
ERROR
  RFC 9457 Problem Detail
API v3
{
  "type": "https://developer.cysmo.de/api/v3/errors#InternalServerError",
  "status": 500,
  "title": "An unexpected error occurred.",
  "instance": "/v3/companies/e1c7...",
  "correlationId": "c927af..."
}

Endpoint Changes

Companies

v2 v3
POST /v2/companies POST /v3/companies
GET /v2/companies GET /v3/companies or POST /v3/companies/get-filtered
GET /v2/companies/{id} GET /v3/companies/{companyId}
DELETE /v2/companies/{id} DELETE /v3/companies/{companyId}
PUT /v2/companies/{id} see below
PATCH /v2/companies/{id} see below
— POST /v3/companies/count-filtered
— GET /v3/companies/{companyId}/ratings
— POST /v3/companies/{companyId}/ratings

When creating a company, you need to use the new company object . The field scanOptions with at least one element in scanTargetDomains or scanTargetNets is required.

The query parameters changed too: acceptSubDomain is now acceptSubdomains, acceptNotReachable no longer exists, and lowPrio=true becomes scanOptions.overridePriority: "LOW".

{
  "portfolioId": "2a9b0ad6-e5de-4239-a282-527c92c7c0f1",
  "data": {
    "name": "My first cysmo company",
    "legalForm": "GENERIC__STOCK_CORPORATION",
    "industry": "A1",
    "numberOfEmployees": 5000,
    "turnover": 72000000,
    "city": "Hamburg",
    "zipCode": "20459",
    "country": "DE",
    "customerStatus": "CUSTOMER",
    "externalReference": "CRM-4711"
  },
  "scanOptions": {
    "scanTargetDomains": ["ppi.de"],
    "overridePriority": "LOW"
  }
}'

Company Updates

The single update endpoint of v2 was split by concern. Each of these returns 204 No Content, so you no longer get the updated company back. Since we are using PUT logic, you already have the updated state at hand, with the following exceptions:

  • modifiedBy and modifiedOn are updated server side
  • updating scan options triggers a new rating, updating the latestRating server side

If you rely on those fields, you need to fetch the updated company via GET.

Concern v3 endpoint
General data PUT /v3/companies/{companyId}/data
Scan options PUT /v3/companies/{companyId}/scan-options
Infrastructure size PUT /v3/companies/{companyId}/scan-options/infrastructure-size
Customer status PUT /v3/companies/{companyId}/data/customer-status
Risk question answers PUT /v3/companies/{companyId}/risk-question-answers
Sales story PUT /v3/companies/{companyId}/sales-story

Ratings and Reports

v2 v3
GET /v2/reports/{ratingId} GET /v3/ratings/{ratingId}
GET /v2/reports/{ratingId}/pdf POST /v3/ratings/{ratingId}/reports/pdf + see below
GET /v2/underwriting/{ratingId} GET /v3/ratings/{ratingId}/rule-evaluations/underwriting
GET /v2/reports/{id}/benchmark not yet published

The technical rating is now fully independent from the view mode, thus the query parameter ?dataMode becomes obsolete for fetching a rating. All sales or underwriting related data is either fetched additionally or included in the rating all the time. The view mode is only needed for generating PDF reports, where it moved from a query parameter to the request body.

Note

The view mode UNDERWRITING was renamed to RULES.
In the future we plan to add a rule set selector in that view mode, but by default the RULES view mode will keep using your underwriting rule set.

Radar

v2 v3
GET /v2/companies/{companyId}/radar GET /v3/companies/{companyId}/radar
POST /v2/companies/{companyId}/radar PUT /v3/companies/{companyId}/radar
— DELETE /v3/companies/{companyId}/radar
— PUT /v3/companies/radar/set-filtered
— POST /v3/companies/radar/delete-filtered
— GET, PUT, DELETE /v3/portfolios/{portfolioId}/default-radar

A portfolios default radar config is applied for all newly created companies in that portfolio.

Translations

The v3 translations endpoint supports the optional ratingVersion query parameter, which filters the messages and strips the number prefix from the labels for you.

GET
  /v2/values/translations?lang=en
API v2
// Response
{
  "12.attack_resilience.vulns.critical.potential_risk": "Attackers can and have very likely already taken over the system.",
  "12.mailAvailability.high": "High",
  "7.mailAvailability.high": "High",
  "ratingGrade": "Rating grade",
  "tables.name.malicious_as": "AS Reputation",
  // ...
}
// Response
{
  "attack_resilience.vulns.critical.potential_risk": "Attackers can and have very likely already taken over the system.",
  "mailAvailability.high": "High",
  "ratingGrade": "Rating grade",
  "tables.name.malicious_as": "AS Reputation",
  // ...
}

Status Codes

PUT and DELETE operations now return status 204 No Content with no response body.

Operation v2 v3
Update a company 200 with body 204
Delete a company 200 204
Set or delete a radar 200 204
Request a PDF report 200 with the file 202 Accepted with a status monitoring link
Request an export — 202 Accepted with a status monitoring link

Filtering and Pagination

You still have the option to use query parameters (GET /v3/companies?country=DE&country=IT&country=FR&pageSize=100&page=2) but now you also have the option to send your query as a JSON object:

{
  "country": [
    "DE",
    "IT",
    "FR"
  ],
  "pageSize": 100,
  "page": 2
}

This is especially useful for long filter queries, which might exceed URL/Header length limits.

Like in v2, within one property values are OR-joined. Across properties, they are AND-joined.

Some filter parameters were renamed for a more consistent experience:

v2 v3 Changed
name name
domain domain
country country
zipcode zipCode *
city city
legalform legalForm *
industry industry
sectorLetter industry (now supports both ‘A’ and ‘A1’) *
numberOfEmployees numberOfEmployeesMin and numberOfEmployeesMax *
turnover turnoverMin and turnoverMax *
insuranceId externalReference *
status customerStatus *
company id *
user createdBy
freeText freeText, now an array *
dateMin, dateMax ratingCreatedOnMin, ratingCreatedOnMax *
valueTechMin, valueTechMax ratingValueMin, ratingValueMax *
valueUnderwriting underwritingEvaluation *
technicalTrendStatus ratingValueTrend *
sort sortBy *
sortOrder sortOrder
page page
pageSize pageSize
— portfolio *
client — *
businessTrendStatus — *
valueBusinessMin, valueBusinessMax — *
— ratingState *

sortBy now has the following options: LATEST_RATING_DATE (default), TREND, COMPANY_NAME, VALUE, FINDINGS and UNDERWRITING_EVALUATION.

Note

pageSize is capped at 100 in v3, where v2 allowed up to 1000. The default also dropped from 100 to 15, so adapt your pageSize if needed.

Rating Priorities

The lowPrio query parameter available in v2’s POST /v2/companies became a Priority enum with the values NORMAL and LOW and is now used in multiple locations.

The defaults differ per operation. Creating a company and update-triggered re-ratings default to NORMAL, so you see the results of your interaction more quickly. Regular re-ratings default to LOW.

You can use the overridePriority or priority fields respectively to differ from the default behavior. The priority is not persisted on the company; it will always fall back to its default if not explicitly given in the request.

Important

Priority NORMAL is rate limited (currently to a maximum of 15 ratings at the same time). Exceeding the rate limit returns 429 with error TooManyHighPriorityRatings. Use the priority LOW for batch operations.

Reports Are Asynchronous

In v2 you fetched a PDF directly and received the file in the response body. In v3 you request the report, then poll for it.

First, request the report:

{
  "viewMode": "TECH",
  "language": "en"
}

The response contains a link to a status endpoint. Poll it until the download link appears:

// Response
{
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "type": "PDF_REPORT",
  "progress": 0,
  "expires": "2019-08-24T14:15:22Z",
  "ratingId": "e2685a8c-cb0d-4ffd-85d1-e4f8966e7aa9",
  "viewMode": "TECH",
  "link": "https://api.cysmo.de/v3/downloads/file/T3LwNczqh430YHy2wrbEMr8UdSfz8MpIMXBqxoTb8T1fl9XfCj5dZ01NhYnUUr/Cysmo_Report_Sample%20Company_technical.pdf"
}

progress runs from 0 to 1. Once it is complete, link holds the download URL. This usually takes a few seconds and rarely more than a minute. If error is set, the request has failed for good and you need to request the content again, ideally with different parameters.

The reducedTo parameter moved from the query string into the request body. zoneOffset has no v3 equivalent.

New in v3

Beyond the changes above, v3 adds capabilities that had no v2 counterpart:

  • Portfolios: Full management through GET and POST /v3/portfolios plus GET, PUT and DELETE /v3/portfolios/{portfolioId}. Your “active portfolio” is the default used for company creation and is available through GET and PUT /v3/users/self/active-portfolio. But we recommend setting the portfolio explicitly in the request body on company creation, to work statelessly.

  • Exports: POST /v3/exports generates a RATING_LIST or RATING_TABLES export for a filtered set of companies, optionally including historic ratings. A user can only have one running export per type. Download of the generated export is handled like with PDFs.

  • Downloads: The new GET /v3/downloads and GET /v3/downloads/{downloadId} serve a central place for every asynchronously generated file. The created file download links (/v3/downloads/file/…) hold a secure token, so they can be used without a cysmo session. This enables you to trigger content generation on your application server while handling content download on the users device.

  • Sales stories: GET, PUT and DELETE /v3/companies/{companyId}/sales-story, used to set the pre-selected illustrative story for PDF reports in the sales view mode.

  • Infrastructure size: PUT /v3/companies/{companyId}/scan-options/infrastructure-size lets you switch a company to LARGE if your previous rating failed with state INFRASTRUCTURE_TOO_LARGE. An immediate re-rating will be triggered automatically.

Benchmarks

The benchmarks object has been cleaned up and the paths changed a bit, but the purpose stays the same: You receive a benchmark value for every score, partial rating and the overall rating and you can filter by different scopes (companies of similar industry or turnover).

GET
  /v2/reports/{reportId}/benchmark?dataMode=TECH&benchmarkOption={scope}
API v2
// With {scope} being any of "ALL", "BRANCH" or "TURNOVER" (case-sensitive)
// Response
{
  "version": 12,
  "value": 0.8123,
  "partialRatings": [
    {
      "nameKey": "attack_resilience",
      "classifications": {
        "CRITICAL": 0,       // All always 0
        "CATASTROPHIC": 0,
        "NEUTRAL": 0,
        "WARNING": 0,
        "GOOD": 0
      },
      "timedOut": false,     // Deprecated. Always false.
      "high": null,          // Deprecated. Always null.
      "low": null,
      "value": 0.9,
      "scores": [
        {
          "nameKey": "attack_resilience.open_ports",
          "value": 0.753,
          // same extra fields as in partial rating
        },
        // ...
      ]
    },
    // ...
  ]
}
// With {scope} being any of "global", "industry" or "turnover" (case-insensitive)

// Response
{
  "version": 12,
  "value": 0.8123,
  "partialRatings": [
    {
      "nameKey": "attack_resilience",
      "value": 0.9,
      "scores": [
        {
          "nameKey": "attack_resilience.open_ports",
          "value": 0.753
        },
        // ...
      ]
    },
    // ...
  ]
}

The error responses were updated as well: If no benchmark information is available you receive one of two 404 responses, which you can distinguish by their types:

  • BenchmarkNotAvailable: You did nothing wrong, cysmo® simply doesn’t have enough comparable data for this rating.
  • ResourceNotFound: Your provided Rating ID doesn’t exist or the rating’s company doesn’t have the necessary data set for the benchmark scope you requested (i.e. industry or turnover is missing).

API v2 End-of-Life Timeline

We are excited to introduce API v3 as the next-generation interface for customer integrations.

API v2 will not receive new features during this period, but existing functionality will remain stable and supported until retirement. We understand that business-critical integrations require careful planning, and we are committed to providing customers with a stable transition window. API v2 will therefore remain stable and supported for one full year following the publication of this migration guide. Prior to retirement, we will notify customers who have not yet migrated with reminder emails and relevant migration information.

Important

The legacy cysmo® API v2 will remain available until September 30, 2027.