POST /api/v1/keys/import
Import keys -- a peer key server's (NATIVE) or your own (RAW, WRAPPED).
IMPORT_KEY in one of three formats, named by format (absent: NATIVE).
RAW and WRAPPED import your own symmetric keys -- from a KMIP key manager, an HSM, a
file -- each under a hidden link the key server mints. The Gateway sends no hidden link
of yours to the key server: a key in the body carrying hiddenLink is 400. So no import
can reach an existing key, and nobody can claim an identifier before its key exists. Name
each key with the optional sourceId (a KMIP unique identifier, say): it is echoed in its
result and never stored, so the mapping from sourceId to hiddenLink is yours to keep.
WRAPPED-- the key bytes wrapped to the RSA public key of this key server's trust certificate, whichGET /api/v1/trust/local-certificatereturns ascertificatePemto the importing principal itself (the Exchanger role carries that read).methodisrsa-oaep-sha256(RSA-OAEP, SHA-256, MGF1-SHA-256, no label, of the key bytes) orrsa-aes-kwp-sha256(RSA-OAEP-SHA256 of a one-time AES-256 key, followed by that key's RFC 5649 wrap of the key bytes -- the layout of PKCS#11CKM_RSA_AES_KEY_WRAP). Needs the trust certificate: without one,409 TRUST_NOT_ESTABLISHED; with an EC one,409 TRUST_KEY_NOT_RSA. Use WRAPPED whenever the source can wrap.RAW-- the key bytes in the clear (inside TLS). Needs no trust certificate. The key passes through the Gateway's memory: use it only on a trusted path.
Per key: cipher (AES-128-CBC, AES-192-CBC or AES-256-CBC, and only those this
key server creates keys for), the key (key for RAW, wrappedKey for WRAPPED; canonical
base64), and an optional iv (absent, the key server mints one). The key server checks the
cipher and the key's length. The answer is 201, one result per key in request order:
{ sourceId?, hiddenLink, outcome: "imported" }, the caller the key's OWNER. A key the key
server refused is { sourceId?, outcome: "refused", code } and nothing was stored for it:
0x0503 (1283) an argument -- method, cipher, IV or key length -- it does not accept,
0x0600 (1536) material that did not unwrap under its trust certificate. If any key was
refused the status is that refusal's -- 400 INVALID_ARGUMENT, 400 KEY_UNWRAP_FAILED --
and the error body carries every result. One audit record per imported key names its
hidden link.
A scoped credential cannot import RAW or WRAPPED: it names the keys it may reach, and a new
key is none of them (403, nothing sent).
NATIVE: one hidden link and one wrapped key blob per key, as a peer key
server's POST /api/v1/keys/export to this key server's recipient produced them.
A NATIVE import needs an established federated trust -- this key server's own external
trust certificate (POST /api/v1/trust/local-certificate/accept). Without one the key server
refuses with "establish trust first", answered 409 TRUST_NOT_ESTABLISHED.
The answer carries one result per key, in request order: imported (the caller is the
key's OWNER on this key server), duplicate (this key server already holds a key under that
hidden link, expired or not, and it is never overwritten) or refused with the key server's
code. Each key is applied independently. If any key was refused the status is that
refusal's, and the error body carries every result. accepted and verified are deprecated
constants; read results.
A key removed from this key server (POST /api/v1/keys/remove) can be imported again from a
copy exported earlier.
For NATIVE a scoped credential must cover every hidden link. Role, every format: Exchanger ("Import Key").
Authentication
Send the session id as Authorization: Bearer <session id>, and an RFC 9449 DPoP proof in the DPoP header if your session is bound to a key. The two are required together, not as alternatives — see Authenticating and Proving you hold your key.
There is no session cookie on any response and none is read on any request. Do not set credentials: "include" on a browser fetch.
Request
| Field | Type | Required | Description |
|---|---|---|---|
format | string (NATIVE, RAW, WRAPPED) | no | Optional; absent is NATIVE. RAW and WRAPPED: your own keys, under minted hidden links. |
keys | array of object | yes | NATIVE: hiddenLink and keyBlob, both required. RAW: cipher and key required, iv and sourceId optional. WRAPPED: cipher, method and wrappedKey required, iv and sourceId optional. A field of another format is 400. |
keys[] object
| Field | Type | Required | Description |
|---|---|---|---|
hiddenLink | string | no | NATIVE only: canonical base64 of the hidden link. Refused for RAW and WRAPPED, whose links the key server mints. |
keyBlob | string | no | NATIVE only: canonical base64 of the wrapped key blob. |
cipher | string (AES-128-CBC, AES-192-CBC, AES-256-CBC) | no | RAW and WRAPPED. The key's cipher; the key server refuses one it does not create keys for. |
key | string | no | RAW: canonical base64 of the key bytes, exactly the cipher's key length. |
method | string (rsa-oaep-sha256, rsa-aes-kwp-sha256) | no | WRAPPED. How wrappedKey was wrapped to the trust certificate's RSA public key. |
wrappedKey | string | no | WRAPPED: canonical base64 of the wrapped key material, as the method produces it. |
iv | string | no | RAW and WRAPPED, optional: canonical base64 of the cipher's IV (16 bytes). Absent, the key server mints one. |
sourceId | string | no | RAW and WRAPPED, optional. Your name for the key; echoed in its result, never stored. |
Example
{
"format": "NATIVE",
"keys": [
{
"hiddenLink": "...",
"keyBlob": "...",
"cipher": "AES-128-CBC",
"key": "...",
"method": "rsa-oaep-sha256",
"wrappedKey": "...",
"iv": "...",
"sourceId": "..."
}
]
}
Responses
201
RAW or WRAPPED: every key was imported, each under a hidden link the key server minted.
| Field | Type | Required | Description |
|---|---|---|---|
format | string (RAW, WRAPPED) | yes | |
keysRequested | integer | yes | |
results | array of ForeignKeyImportResult | yes | What became of one key of a RAW or WRAPPED import, in request order. Carries no key material. |
detail | string | no |
Example
{
"format": "RAW",
"keysRequested": 0,
"results": [
{
"sourceId": "...",
"hiddenLink": "...",
"outcome": "imported",
"code": 0
}
],
"detail": "..."
}
200
NATIVE. Every key was imported or found already present. results says which.
| Field | Type | Required | Description |
|---|---|---|---|
format | "NATIVE" (constant) | yes | |
keysRequested | integer | yes | |
accepted | true (constant) | yes | |
verified | false (constant) | yes | |
results | array of KeyImportResult | yes | What became of one key of an import request, in request order. |
detail | string | no |
Example
{
"format": "NATIVE",
"keysRequested": 0,
"accepted": true,
"verified": false,
"results": [
{
"hiddenLink": "...",
"outcome": "imported",
"code": 0
}
],
"detail": "..."
}
400
The request was refused before anything was sent to the key server.
Most 400s on this surface carry code: BAD_REQUEST and a detail naming the field and
saying what the key server would otherwise have done with it. A large share of them exist
because the value would have been ACCEPTED downstream and quietly meant something else --
an empty string that reaches the wire as "mint a new key", a zero-length identifier that
names no object, a numeric flag word that clears bits the caller did not name.
INVALID_ARGUMENT is the one 400 that DID reach the key server: the key server itself
refused the argument (ERR_INVALID_ARGUMENT), and detail is its own sentence. On
POST /api/v1/certificates/enroll that is a certificate that cannot authenticate --
certificate not permitted for authentication: keyUsage is present without digitalSignature (...), ... extendedKeyUsage is present without clientAuth (...), or
certificate expired at enrolment: notAfter <UTC> ... -- and a multi-certificate body
that carries one such leaf persists nothing.
KEY_UNWRAP_FAILED reached the key server too, on POST /api/v1/keys/import with format
WRAPPED: the wrapped key did not unwrap under the key server's trust certificate --
wrapped to another key, by another method, or damaged. Nothing was stored for that key.
401
No live session, a DPoP proof that did not verify, or a certificate session whose certificate the key server now refuses.
SESSION_NOT_FOUND -- the credential names no session this Gateway holds.
SESSION_EXPIRED -- it aged out. Both carry reauth: true; obtain a new credential
rather than retrying.
PROOF_* -- the sender constraint refused. See the DPoP section of this document for the
full table; PROOF_MALFORMED is a 400 and PROOF_REPLAY_CACHE_FULL a 503, and the
rest are here.
SCOPED_CREDENTIAL_EXPIRED -- a scoped browser credential passed its fixed lifetime. Use
does not extend it: an idle timeout would be refreshed by exactly the traffic a stolen
handle produces.
CERTIFICATE_REVOKED -- on a session established by certificate login, the key server
re-checks the certificate that session was established with on its operations, and it
is now named by revocation evidence. detail is the key server's sentence. The
Gateway answers this once and then ENDS everything that service holds or minted here --
its sessions, the delegated sessions and the scoped credentials it minted -- so a later
operation on any of them is SESSION_NOT_FOUND. Log in with a different certificate.
SSO_SESSION_EXPIRED -- a single-sign-on session (POST /api/v1/session/oidc) reached
its lifetime, or was idle past its binding's idle bound; it has been ended. Sign in again.
SENDER_KEY_RETIRED -- the session's registered sender key was rotated out and its
overlap has passed (see "The registered service's credential lifecycle"); the session is
closed with this answer. SERVICE_REGISTRATION_ENDED -- the registration a service
credential was issued under was removed or narrowed on this Gateway; it has ended. Both
are final for that credential.
403
Authenticated, and refused. The codes differ in what they say about where the refusal
came from: ACCESS_DENIED (no role grants the operation), ACL_DENIED (the ACL of the
key the request named refuses the principal -- ask the key's owner, not an administrator),
PRINCIPAL_DISABLED and DELEGATOR_REFUSED are the key server's -- the last one on a DELEGATED session whose delegator has since been disabled,
lost its DELEGATION flag, been unenrolled or had its certificate revoked; its
detail is the key server's sentence naming the delegator, and no new login helps until
an administrator restores it. REVOCATION_STATUS_UNAVAILABLE and
REVOCATION_EVIDENCE_STALE are the key server's too, on a session established by
certificate login: the same re-check found no current revocation evidence for that
certificate's issuer under a policy that requires it -- the credential is not
what was refused, and an operator importing a current CRL for the issuer is what helps.
TRANSPORT_PRINCIPAL_REQUIRED (POST /api/v1/keys/get: the operation needs a session
established by a certificate the key server has enrolled) and PRINCIPAL_MISMATCH (the
request named a principal other than the session's) are the key server's too, and no role
changes either -- while
NOT_WRITABLE, BUILT_IN_PRINCIPAL, OWN_ACCOUNT and
OBJECT_NOT_IN_CREDENTIAL_SCOPE are decided by the Gateway before anything is sent.
409
TRUST_NOT_ESTABLISHED -- this key server holds no external trust certificate, so it
imports nothing NATIVE or WRAPPED. TRUST_KEY_NOT_RSA -- WRAPPED, and the trust
certificate's key is EC: both WRAPPED methods are RSA.
500
A fault rather than a refusal.
OP_FAILED is the catch-all: the key server refused in words that match no classified
pattern, so the Gateway cannot say more than that the operation did not happen. Some key
server refusals that are conceptually a caller error arrive here rather than as a 4xx,
because the wire carries no distinguishing code -- where a route can recognise one from
its message it maps it, and each such mapping is documented on the operation.
503
A capacity ceiling or a missing deployment prerequisite. retryable says which.
A scope of session or process names the ceiling that was hit, and the response
carries Retry-After.
BUILD_NOT_ADMITTED (scope: process, retryable: true, Retry-After): the key server
does not admit this Gateway's client build. Every key-server connection the Gateway opens
attests the signed release it runs (TE_RELEASE_MANIFEST) right after it authenticates;
while that attestation is refused -- the release is not registered at the key server's
code-signing level, the level was raised under it, or the installed files no longer
match the signed manifest -- every route but GET /api/v1/health and GET /api/v1/ready
answers this, and detail names the cause. It is never a 401 or a 403: no credential
or role of the caller's is at fault. The Gateway re-attempts on a timer and recovers
without a restart once the key server admits it; GET /api/v1/ready says when.
CAPACITY_UNAVAILABLE (scope: process, retryable: true, Retry-After): this Gateway
is serving its maximum number of requests at once, or cannot track another allowance, or
(on a certificate login) is recording its maximum of live certificate sessions -- in which
case the session the login created has been closed. Nothing else was sent to the key
server. See "Rate and capacity limits" above.
413
PAYLOAD_TOO_LARGE (retryable: false) -- the request body is larger than this
deployment accepts. It was refused before it was parsed, and the connection is closed.
Nothing was sent to the key server.
429
RATE_LIMITED (retryable: true, Retry-After) -- the allowance this request is charged
to is spent: the login allowance of its network source (before authentication), or the
allowance, the concurrency ceiling or the live-session ceiling of the identity it was
verified as -- shared by every session that identity holds, including every delegated
and scoped credential a service minted. It carries no scope. Nothing was sent to the
key server; on a login refused after authentication, the session it created has been
closed. See "Rate and capacity limits" above.
Error handling
Every failure answers JSON carrying at least code and detail. Match on code — detail is written for a human debugging the call and its wording is not part of the contract. See the error model.
| Status | Meaning |
|---|---|
400 | The request was refused before anything was sent to the key server. Most 400s on this surface carry code: BAD_REQUEST and a detail naming the field and saying what the key server would otherwise have done with it. A large share of them exist because the value would have been ACCEPTED downstream and quietly meant something else -- an empty string that reaches the wire as "mint a new key", a zero-length identifier that names no object, a numeric flag word that clears bits the caller did not name. INVALID_ARGUMENT is the one 400 that DID reach the key server: the key server itself refused the argument (ERR_INVALID_ARGUMENT), and detail is its own sentence. On POST /api/v1/certificates/enroll that is a certificate that cannot authenticate -- certificate not permitted for authentication: keyUsage is present without digitalSignature (...), ... extendedKeyUsage is present without clientAuth (...), or certificate expired at enrolment: notAfter <UTC> ... -- and a multi-certificate body that carries one such leaf persists nothing. KEY_UNWRAP_FAILED reached the key server too, on POST /api/v1/keys/import with format WRAPPED: the wrapped key did not unwrap under the key server's trust certificate -- wrapped to another key, by another method, or damaged. Nothing was stored for that key. |
401 | No live session, a DPoP proof that did not verify, or a certificate session whose certificate the key server now refuses. SESSION_NOT_FOUND -- the credential names no session this Gateway holds. SESSION_EXPIRED -- it aged out. Both carry reauth: true; obtain a new credential rather than retrying. PROOF_* -- the sender constraint refused. See the DPoP section of this document for the full table; PROOF_MALFORMED is a 400 and PROOF_REPLAY_CACHE_FULL a 503, and the rest are here. SCOPED_CREDENTIAL_EXPIRED -- a scoped browser credential passed its fixed lifetime. Use does not extend it: an idle timeout would be refreshed by exactly the traffic a stolen handle produces. CERTIFICATE_REVOKED -- on a session established by certificate login, the key server re-checks the certificate that session was established with on its operations, and it is now named by revocation evidence. detail is the key server's sentence. The Gateway answers this once and then ENDS everything that service holds or minted here -- its sessions, the delegated sessions and the scoped credentials it minted -- so a later operation on any of them is SESSION_NOT_FOUND. Log in with a different certificate. SSO_SESSION_EXPIRED -- a single-sign-on session (POST /api/v1/session/oidc) reached its lifetime, or was idle past its binding's idle bound; it has been ended. Sign in again. SENDER_KEY_RETIRED -- the session's registered sender key was rotated out and its overlap has passed (see "The registered service's credential lifecycle"); the session is closed with this answer. SERVICE_REGISTRATION_ENDED -- the registration a service credential was issued under was removed or narrowed on this Gateway; it has ended. Both are final for that credential. |
403 | Authenticated, and refused. The codes differ in what they say about where the refusal came from: ACCESS_DENIED (no role grants the operation), ACL_DENIED (the ACL of the key the request named refuses the principal -- ask the key's owner, not an administrator), PRINCIPAL_DISABLED and DELEGATOR_REFUSED are the key server's -- the last one on a DELEGATED session whose delegator has since been disabled, lost its DELEGATION flag, been unenrolled or had its certificate revoked; its detail is the key server's sentence naming the delegator, and no new login helps until an administrator restores it. REVOCATION_STATUS_UNAVAILABLE and REVOCATION_EVIDENCE_STALE are the key server's too, on a session established by certificate login: the same re-check found no current revocation evidence for that certificate's issuer under a policy that requires it -- the credential is not what was refused, and an operator importing a current CRL for the issuer is what helps. TRANSPORT_PRINCIPAL_REQUIRED (POST /api/v1/keys/get: the operation needs a session established by a certificate the key server has enrolled) and PRINCIPAL_MISMATCH (the request named a principal other than the session's) are the key server's too, and no role changes either -- while NOT_WRITABLE, BUILT_IN_PRINCIPAL, OWN_ACCOUNT and OBJECT_NOT_IN_CREDENTIAL_SCOPE are decided by the Gateway before anything is sent. |
409 | TRUST_NOT_ESTABLISHED -- this key server holds no external trust certificate, so it imports nothing NATIVE or WRAPPED. TRUST_KEY_NOT_RSA -- WRAPPED, and the trust certificate's key is EC: both WRAPPED methods are RSA. |
500 | A fault rather than a refusal. OP_FAILED is the catch-all: the key server refused in words that match no classified pattern, so the Gateway cannot say more than that the operation did not happen. Some key server refusals that are conceptually a caller error arrive here rather than as a 4xx, because the wire carries no distinguishing code -- where a route can recognise one from its message it maps it, and each such mapping is documented on the operation. |
503 | A capacity ceiling or a missing deployment prerequisite. retryable says which. A scope of session or process names the ceiling that was hit, and the response carries Retry-After. BUILD_NOT_ADMITTED (scope: process, retryable: true, Retry-After): the key server does not admit this Gateway's client build. Every key-server connection the Gateway opens attests the signed release it runs (TE_RELEASE_MANIFEST) right after it authenticates; while that attestation is refused -- the release is not registered at the key server's code-signing level, the level was raised under it, or the installed files no longer match the signed manifest -- every route but GET /api/v1/health and GET /api/v1/ready answers this, and detail names the cause. It is never a 401 or a 403: no credential or role of the caller's is at fault. The Gateway re-attempts on a timer and recovers without a restart once the key server admits it; GET /api/v1/ready says when. CAPACITY_UNAVAILABLE (scope: process, retryable: true, Retry-After): this Gateway is serving its maximum number of requests at once, or cannot track another allowance, or (on a certificate login) is recording its maximum of live certificate sessions -- in which case the session the login created has been closed. Nothing else was sent to the key server. See "Rate and capacity limits" above. |
413 | PAYLOAD_TOO_LARGE (retryable: false) -- the request body is larger than this deployment accepts. It was refused before it was parsed, and the connection is closed. Nothing was sent to the key server. |
429 | RATE_LIMITED (retryable: true, Retry-After) -- the allowance this request is charged to is spent: the login allowance of its network source (before authentication), or the allowance, the concurrency ceiling or the live-session ceiling of the identity it was verified as -- shared by every session that identity holds, including every delegated and scoped credential a service minted. It carries no scope. Nothing was sent to the key server; on a login refused after authentication, the session it created has been closed. See "Rate and capacity limits" above. |