Connecting your billing or CRM system to the Uniqkey Partner API

Uniqkey partners can read monthly license usage for all their customer organizations through a REST API, so a billing, CRM, or reporting system can pick up the numbers directly instead of someone exporting them from the Partner Portal. This article explains how to create an API key in the Partner Portal, how the API works, and what to expect from the data.

Who can use this

  • The API is for Uniqkey partners. API keys are created in the Partner Portal by a partner user with the Admin role. Other roles do not see the API keys page.
  • A key belongs to your partner account, not to the user who created it. It keeps working if that user leaves or is removed.
  • A key sees your own partner account and the partners below it in your hierarchy, so a distributor also sees its resellers' customers. It never sees partners above or next to yours.
  • Your system needs outbound HTTPS access to https://partner-api-public.production.uniqkey.eu.
  • The API is read-only and returns license figures and organization names. No vault data, passwords, keys, or employee details are ever included.

Step 1: Create an API key

  1. Log in to the Partner Portal as an Admin.
  2. Open API keys in the menu.
  3. Click Create API key.
  4. Enter a Name for the integration that will use the key, for example Billing sync. The name is shown in the audit log for everything the key does, so make it recognizable.
  5. Optionally pick an Expiry date. The earliest date you can pick is tomorrow. The key works until the end of that day in your browser's time zone. Leave the field empty for a key that does not expire.
  6. Click Create. The Copy your API key now dialog shows the new key, which starts with uk_p_.
  7. Copy the key and store it in your system's secret store. The key is shown only once. Tick I have stored this API key in a safe place to close the dialog. After that the key cannot be displayed again. If you lose it, create a new key.

Managing keys later:

  • The API keys page lists every key with its name, the first characters of the key (so you can tell keys apart), when it was created, when it expires, when it was last used, and its state: Active, Expired, or Revoked.
  • To revoke a key, open the menu on its row, choose Revoke, and confirm. Revoking is permanent and cannot be undone. The key stops working immediately; in rare cases it can take up to 1 minute. Revoked keys stay in the list with the state Revoked.
  • Last used is updated at most every 5 minutes, so a key in active use can show a time up to 5 minutes old.
  • An expiry date cannot be changed after the key is created. When a key is about to expire, create a new one.
  • You can create several keys, for example one per integration, so you can revoke one without affecting the others. All keys of a partner share one rate limit (see "Rate limits").
  • To rotate a key: create a new key, update your system, check that Last used shows the new key in use, then revoke the old one.
  • Creating and revoking a key are recorded under Audit logs in the Partner Portal, with the key name. Every successful license data request made with a key is recorded there too, with the key name shown as the actor. The entries appear in the audit log of the partner that owns the key. Rejected requests and requests to /v1/me are not recorded.

Step 2: Configure your system

Every integration needs the same things:

SettingValue
Base URLhttps://partner-api-public.production.uniqkey.eu
RequestGET /v1/licenses
AuthenticationHTTP header Authorization: Bearer <your key>
PagingWhile hasMore is true, send the nextCursor from the response back as the cursor query parameter, with the same from, to, and limit.

Pulling the data once a day is a good default. The figures are updated by a daily job (see "What to expect" below), so polling more often gains nothing.

Interactive API documentation is available at https://partner-api-public.production.uniqkey.eu/docs. The OpenAPI specification, which most tools can use to generate a client, is at https://partner-api-public.production.uniqkey.eu/openapi/v1.json.

Quick test with curl

Check that the key works and which partner it belongs to:

curl -s "https://partner-api-public.production.uniqkey.eu/v1/me" \
  -H "Authorization: Bearer <your key>"

Then fetch the first few license rows:

curl -s "https://partner-api-public.production.uniqkey.eu/v1/licenses?limit=5" \
  -H "Authorization: Bearer <your key>"

A working setup returns HTTP 200 with a JSON body like the ones in the "Response" section. 401 means the Authorization header is missing, is not a Bearer token, or the key is wrong, revoked, or expired.

API reference

Request

GET /v1/licenses

Query parameterDescription
fromEarliest month to include, inclusive, as yyyy-MM (for example 2026-01). The format is strict: 2026-1 or 2026-01-15 is rejected.
toLatest month to include, inclusive, as yyyy-MM. Must not be earlier than from.
limitRows per page, from 1 to 1000. Default 100. Values outside the range are rejected, not clamped.
cursorThe nextCursor from the previous page. Send it back unchanged and keep the other parameters the same.

A request with no parameters is valid and returns every month of every organization the key can see, newest first, one page at a time.

The cursor is opaque. Do not build, parse, or modify it. Its format may change without notice.

GET /v1/me

Takes no parameters. Returns the partner the key belongs to and the key's own name, prefix, and expiry. Use it to test a key. It counts toward the rate limit like any other request.

Response

GET /v1/licenses:

{
  "items": [
    {
      "organizationId": "d5ecd732-4a67-418c-9dea-38a097fba1f6",
      "organizationName": "Example Customer ApS",
      "organizationExternalId": "CRM-10442",
      "partnerId": "3f2a9c1e-7b44-4d0a-9e51-2a6c8f0d1b77",
      "partnerName": "Example Reseller A/S",
      "month": "2026-09",
      "usageLicenseCount": 48,
      "committedLicenseCount": 50,
      "discoveryInstallationCount": 12,
      "discoveryActivatedNow": true
    }
  ],
  "hasMore": true,
  "nextCursor": "eyJEIjoiMjAyNi0wOS0wMSIsIk8iOiJkNWVj..."
}
  • Rows are sorted newest month first, then by organizationId.
  • hasMore tells you whether to request the next page. Loop on hasMore.
  • nextCursor is null on the last page.

GET /v1/me:

{
  "partnerId": "3f2a9c1e-7b44-4d0a-9e51-2a6c8f0d1b77",
  "partnerName": "Example Reseller A/S",
  "keyName": "Billing sync",
  "keyPrefix": "uk_p_Xa81kQ2z",
  "expiresAt": "2027-03-31T21:59:59.999Z"
}

expiresAt is in UTC and is null for a key that does not expire.

Errors use the standard problem details format (Content-Type: application/problem+json) with a stable, machine-readable code:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "The request could not be processed.",
  "code": "bad_request",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Build your error handling on code and the HTTP status. title and detail are for people and may change.

CodeStatusMeaning
bad_request400A query parameter is invalid: from or to is not yyyy-MM, from is later than to, limit is outside 1 to 1000, or the cursor was modified. Check the parameters against the table above.
validation_failed400A parameter has the wrong type, for example a limit that is not a number. The response lists the field under errors, and its title is "One or more validation errors occurred." instead of "Bad Request".
unauthorized401The Authorization header is missing or not a Bearer token, or the key is wrong, revoked, or expired.
not_found404The URL is wrong, for example a typo in the path. In rare cases also returned right after a partner account has been deleted.
too_many_requests429The rate limit was exceeded. See "Rate limits".
internal_error500Something went wrong on our side. Retry later and contact support with the traceId if it persists.

Row format

FieldDescription
organizationIdThe customer organization's id. Stable. Use it, together with month, as the key for each row.
organizationNameThe organization's current name, not the name it had in that month.
organizationExternalIdYour own reference for the organization, if you entered one in the Partner Portal. Usually null. Useful for matching rows to customers in your CRM.
partnerId, partnerNameThe partner the organization currently belongs to: your own partner account or one of the partners below it. partnerName is the current name.
monthThe month the row covers, as yyyy-MM.
usageLicenseCountLicensed users: employees who are active, invited, or staged. Uniqkey support accounts are not counted. The highest daily count recorded during the month.
committedLicenseCountThe organization's license commitment. The highest commitment during the month. null when there is no commitment or the commitment is 0.
discoveryInstallationCountDiscovery installations: passive browser extension installations that were active in the last 30 days and have not yet been onboarded as a full Uniqkey user. The highest daily count recorded during the month. This is not the "Corporate identities" figure on the Discovery dashboard.
discoveryActivatedNowWhether Discovery is switched on for the organization right now. This is current state, not the state during the month, so it has the same value on every row of the same organization. Do not bill on it.

Rate limits

  • Each partner can make at least 120 requests per minute. The limit is shared by all keys of the partner, so creating more keys does not add capacity.
  • When the limit is exceeded the API returns 429 with the too_many_requests code and a Retry-After header with the number of seconds to wait. Wait at least that long before retrying.
  • A separate, higher limit per source IP address is applied at the network edge. Normal responses carry X-RateLimit-* headers for that limit. When it is exceeded you also get 429, but without a JSON body, so handle a 429 by its status and Retry-After, not by parsing the body.

A daily sync with limit=1000 needs only a handful of requests and stays well inside the limits.

What to expect from the data

  • Counts are monthly peaks. usageLicenseCount and discoveryInstallationCount are the highest of the daily counts recorded during the month, not an average or the value at month end. A user who is added and removed again between two daily counts is not seen.
  • Data lags by up to a day. The figures are recorded by a daily job that runs shortly after midnight UTC and counts toward the previous day. A change made today shows up after the next run.
  • The current month can still change. Until the month is over, a new peak raises the current month's row. The last count for a month is taken in the first hour of the 1st of the next month (UTC), so treat a month as final from 02:00 UTC on the 1st.
  • Every organization gets a row every month, including months with zero usage. Rows start in the month the organization was created. In rare cases, such as a failed daily run, a month can be missing for an organization. Treat a missing month as unknown, not as zero.
  • Trial organizations are included. Rows are returned for every organization under your partner account, whether or not it is a paying customer.
  • No commitment and a commitment of 0 look the same. Both are returned as committedLicenseCount: null.
  • discoveryInstallationCount is installations, not identities. It counts passive extension installations, which is what Discovery is licensed on. The "Corporate identities" number on the Discovery dashboard counts something else and will not match.
  • discoveryActivatedNow is not historical. It reflects the setting today and is repeated on every month. Use discoveryInstallationCount for billing.
  • Names are current names. If an organization or partner is renamed, all of its months show the new name. Match on organizationId, not on names.
  • Deleted organizations keep their history. Months recorded before an organization was deleted are still returned. The rows do not show that the organization has been deleted.
  • Moving an organization moves its history. Only Uniqkey can move an organization to another partner. If that happens, all of its months, including months already billed, follow it to the new partner. They disappear from the old partner's results unless the new partner is below the old one.

Notes for billing and CRM integrations

  • Keep your own copy of billed months. Store each month's rows in your system when you bill. Because of the moves and renames described above, a month you query again later can differ from what you billed.
  • Use organizationId and month as the key. Every organization has at most one row per month. Update the row on repeated syncs instead of inserting duplicates.
  • Re-read recent months. A simple, robust sync is to request from set to the previous month on every run and overwrite those rows, then lock a month in your system once it has been billed.
  • Map customers with organizationExternalId. In the Partner Portal, open the organization, edit its details, and enter your CRM's customer number in External ID. You can also set it when you create an organization. It then comes back on every row. Uniqkey does not check that the value is unique.
  • Use one key per system. A separate key for billing and for CRM makes the audit log easier to read and lets you revoke one without breaking the other.

Troubleshooting

SymptomLikely causeFix
401 UnauthorizedNo Authorization header, it does not start with Bearer, or the key is wrong, revoked, or expiredCheck the header format; check the key's state on the API keys page and create a new key if needed
400 bad_requestfrom or to is not yyyy-MM, from is after to, limit is outside 1 to 1000, or the cursor was modifiedFix the parameters; when paging, send nextCursor back unchanged
429 Too Many RequestsMore than the rate limit within a minuteWait for the number of seconds in Retry-After, then continue; spread requests out
An organization is missingIt belongs to a partner above or next to yours, it was moved to a partner outside your hierarchy, it was created today and the daily job has not run yet, or your from and to range excludes itCheck which partner the organization belongs to in the Partner Portal, and check the date range
An organization's past months disappearedIt was moved to another partner, which takes its whole history alongUse the copy of billed months kept in your own system
The current month changes between syncsNormal: the row holds the peak so far and the month is not overTreat a month as final from 02:00 UTC on the 1st of the next month
Today's change is not visibleThe data is updated by a daily jobCheck again the next day
discoveryInstallationCount does not match the Discovery dashboardThe dashboard's "Corporate identities" counts something elseUse discoveryInstallationCount for billing
A revoked key still works for a short whileIn rare cases a revocation takes up to 1 minute to take effectWait a minute
API keys is not in the Partner Portal menuYour role is not AdminAsk a partner Admin to create the key

When contacting support, include your partner id (from /v1/me), the key name, the exact request URL without the key, the HTTP status, and the response body including the traceId.