Skip to content
rev3rse

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.

MethodRouteAuthDoes
POST/v1/releasesCIBegin 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}/registerCIRegister every reserved key with the sealed files’ hashes. Commits the keys, the grants and the receipt together, or nothing.
GET/v1/releases/{id}CIThe release’s state; once committed, its receipt.
POST/v1/releases/{id}/abandonCIGive up a draft. Its reserved versions are never handed out again.
POST/v1/activation/challengenoneA single-use challenge for one activation or renewal.
POST/v1/activation/activatedevice keysAttested device keys plus an activation code or a voucher, for a licence wrapping the asset keys to this device.
POST/v1/activation/renewdevice keysA fresh lease for an active installation.
POST/v1/activation/deactivatedevice keysReleases the installation’s slot.
GET/v1/licence-keysnoneThe 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"
    }
  ]
}
FieldMeaning
operation_idNames 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_idMust be the app the credential publishes for.
sourceOptional: provider, repository, ref, commit, run_url. Recorded with the release as the runner reported it.
assets[].asset_idThe id your app opens.
assets[].asset_versionYour version of the asset.
assets[].key_idOptional; defaults to asset_id. Owned by the first app that publishes it.
assets[].plaintext_sha256Binds the reservation to the input that will be sealed.
assets[].entitlementsEntitlements 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_id and 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"
}
errorStatusMeans
malformed400The request does not parse or breaks a stated limit.
unauthorized401No 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_*).
policy403Valid, 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_found404No such release for this credential’s app. Another app’s releases do not exist for you.
conflict409The same operation id or release, with a different body.
rate_limited429Slow down and retry.
internal500Retry with backoff. Every release call is idempotent, so a retry is safe.

Limits

Limits

LimitValue
Assets per release64
Releases begun and not finished, per app16
Grants per entitlement64
Entitlements per asset16
Request body64 KiB
CI token lifetime1–366 days; 90 by default
Binding lifetime1–366 days; 366 by default
Bindings per repository16, 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.