Documentation
IP Vault API
Two surfaces on one service. Your CI publishes sealed assets through the release API, with an app-scoped credential. Your app never calls the service itself: the SDK speaks the activation API for it.
Base address: https://vault.rev3rse.dev. JSON in and out, over HTTPS only. The integration guide shows the same calls made for you by the GitHub Action and ipvault release.
| Method | Route | Auth | Does |
|---|---|---|---|
POST | /v1/releases | CI | Begin a release, or resume one by operation_id. Reserves a new key version per asset and returns the ingestion public key. |
POST | /v1/releases/{id}/register | CI | Register every reserved key with the sealed files’ hashes. Commits the keys, the grants and the receipt together, or nothing. |
GET | /v1/releases/{id} | CI | The release’s state; once committed, its receipt. |
POST | /v1/releases/{id}/abandon | CI | Give up a draft. Its reserved versions are never handed out again. |
POST | /v1/activation/challenge | none | A single-use challenge for one activation or renewal. |
POST | /v1/activation/activate | device keys | Attested device keys plus an activation code or a voucher, for a licence wrapping the asset keys to this device. |
POST | /v1/activation/renew | device keys | A fresh lease for an active installation. |
POST | /v1/activation/deactivate | device keys | Releases the installation’s slot. |
GET | /v1/licence-keys | none | The public keys licences are verified with. Safe to embed in your app. |
Authentication
CI credentials
Release calls carry Authorization: Bearer with one of two credentials, each publishing for one app into the entitlements it was created for. Revocation and expiry are checked on every request.
- The job’s OIDC token, from GitHub Actions or GitLab CI, admitted through a binding. The service checks the signature against the key the provider publishes, the issuer, that the audience is this service and nothing else, and the expiry. The binding then matches the repository and its owner by numeric id, never by name, and the ref, environment and workflow it names.
- A CI token,
ipvk_<id>_<secret>. It is shown once, when it is created; the service keeps only its SHA-256. Keep it in your CI’s secret store and pass it in the environment, never as a command-line argument.
What a credential cannot do, by construction: read a key, register a version it did not reserve, replace a registered version, remove a grant, or see or touch another app’s releases or key ids. Each mutation is in the tenant’s hash-chained audit log under the credential’s id, with the job’s repository, ref, run and commit for OIDC; the receipt names the job too. Neither credential is an end-user credential, and neither belongs in an app.
Release API
Begin a release
POST /v1/releases
Authorization: Bearer <the job's OIDC token, or ipvk_...>
Content-Type: application/json
{
"operation_id": "gh-123456-987654-1-android",
"platform": "android",
"app_id": "com.example.app",
"source": {
"provider": "github",
"repository": "example/app",
"ref": "refs/tags/v2.4.0",
"commit": "9fceb02d0ae598e95dc970b74767f19372d61af8",
"run_url": "https://github.com/example/app/actions/runs/987654"
},
"assets": [
{
"asset_id": "model:classifier",
"asset_version": 3,
"plaintext_sha256": "<64 hex>",
"entitlements": ["premium"],
"mode": "W"
}
]
}| Field | Meaning |
|---|---|
operation_id | Names one attempt, 1–128 characters of A–Z a–z 0–9 . _ : -. The same id with the same body returns the same release; with a different body, 409 conflict. The CLI derives it from the CI run, its attempt and the job. |
platform, app_id | Must be the app the credential publishes for. |
source | Optional: provider, repository, ref, commit, run_url. Recorded with the release as the runner reported it. |
assets[].asset_id | The id your app opens. |
assets[].asset_version | Your version of the asset. |
assets[].key_id | Optional; defaults to asset_id. Owned by the first app that publishes it. |
assets[].plaintext_sha256 | Binds the reservation to the input that will be sealed. |
assets[].entitlements | Entitlements to add the new key version to; each must be allowed by the credential. |
assets[].mode | "W" (default) or "H" (secure import where the device supports it). |
The answer reserves a key_version for each asset (the service allocates versions; you never choose one) and carries the tenant’s ingestion public key:
200 OK
{
"release_id": "rel_6f1d2c9a0b8e4f7a5c3d1e2f",
"state": "draft",
"tenant": "<your tenant>",
"platform": "android",
"app_id": "com.example.app",
"operation_id": "gh-123456-987654-1-android",
"credential": "<binding or token id>",
"job": { "provider": "github", "repository_id": "123456", "ref": "refs/tags/v1.4.0", "run": "987654/1", ... },
"source": { "provider": "github", "repository": "example/app", ... },
"assets": [
{
"asset_id": "model:classifier",
"asset_version": 3,
"key_id": "model:classifier",
"key_version": 7,
"plaintext_sha256": "<64 hex>",
"entitlements": ["premium"],
"mode": "W",
"ciphertext_sha256": null,
"ciphertext_size": null
}
],
"created_at": <unix seconds>,
"finished_at": null,
"ingest_public_key": "<base64url, uncompressed P-256>"
}Seal each asset under a fresh key for exactly that key_id and key_version, and wrap the key to the ingestion key. ipvault release publish does both; the wrap is bound to the tenant and the key reference, so a key wrapped for another version is refused at registration.
Release API
Register and commit
POST /v1/releases/rel_6f1d2c9a0b8e4f7a5c3d1e2f/register
Authorization: Bearer <the job's OIDC token, or ipvk_...>
Content-Type: application/json
{
"keys": [
{
"key_id": "model:classifier",
"key_version": 7,
"wrapped": "<base64url: the asset key, wrapped to the ingestion key>",
"ciphertext_sha256": "<64 hex: the sealed file>",
"ciphertext_size": 8424133
}
]
}Exactly the reserved keys, each once. The service unwraps each key into its key custody first, then commits every key, the new grants and the receipt in one transaction — or nothing, if any part fails. Grants are only added: licences for versions you already shipped keep working. The answer is the release with state: "committed" and each asset’s ciphertext_sha256: the receipt.
The sealed file’s hash is reported by your runner; the service never receives the file or its plaintext. The check after the build compares the package against these hashes.
Release API
Read and abandon
GET /v1/releases/{id} returns the release in the same shape: draft, committed (with the receipt) or abandoned. POST /v1/releases/{id}/abandon gives up a draft; a committed release cannot be abandoned, and repeating an abandon returns the abandoned release.
Retries
Idempotency and recovery
- Begin with the same
operation_idand body returns the same release, in whatever state it reached. A lost answer is recovered by asking again. - Register with the same body returns the committed receipt. A different body for a committed release is
409 conflict: a re-seal is a new key, and a new key needs a new release. - Keep the sealed files and the register body until the receipt arrives. If the sealed files are lost after a commit, publish as a new attempt; they cannot be rebuilt under a key that is already registered.
Errors
Errors
Every error has the same shape: a class, a machine-readable reason and a sentence for the person reading the log.
409 Conflict
{
"error": "conflict",
"reason": "operation_id",
"detail": "this operation id began a release with a different request; use a new operation id"
}| error | Status | Means |
|---|---|---|
malformed | 400 | The request does not parse or breaks a stated limit. |
unauthorized | 401 | No credential, an invalid one, a CI token that is revoked (reason revoked) or expired (reason expired), or an OIDC token that is forged, expired, for another audience or from an untrusted issuer (reasons oidc_*). |
policy | 403 | Valid, but not allowed: another app, an entitlement outside the credential’s scope, a key id owned by another app, a limit reached, an abandoned release, a job no binding admits (oidc_binding), a pull request job (untrusted_event) or an unprotected GitLab ref (unprotected_ref). |
not_found | 404 | No such release for this credential’s app. Another app’s releases do not exist for you. |
conflict | 409 | The same operation id or release, with a different body. |
rate_limited | 429 | Slow down and retry. |
internal | 500 | Retry with backoff. Every release call is idempotent, so a retry is safe. |
Limits
Limits
| Limit | Value |
|---|---|
| Assets per release | 64 |
| Releases begun and not finished, per app | 16 |
| Grants per entitlement | 64 |
| Entitlements per asset | 16 |
| Request body | 64 KiB |
| CI token lifetime | 1–366 days; 90 by default |
| Binding lifetime | 1–366 days; 366 by default |
| Bindings per repository | 16, one per app |
Activation API
What the SDK does for your app
Once per installation, the SDK fetches a single-use challenge, creates keys in the device’s hardware key store with attestation over that challenge, and activates with the activation code your customer entered or a voucher your backend signed. The answer is a licence: signed by the service, bound to this app, its signing certificate and this device, and carrying each granted asset key wrapped to the device’s hardware key. Every open after that is local and offline. When the status says the lease has run out, your app calls renew(); the SDK verifies every licence against the keys from /v1/licence-keys.
You do not call these routes yourself. If you are building a client of your own, talk to us first: the protocol is precise about signing inputs, attestation and the device clock, and the SDK is the reference for it.