The flowvault module is a Skyflow Java SDK built for high-throughput vault operations. It shares its client, credentials, and configuration classes with the skyvault SDK (both depend on the common module) but exposes a different, narrower surface: bulk vault operations only.
Meant for Flow Vault.
flowvaultis versioned independently ofskyvault.flowvaultis on a1.xline whileskyvault(com.skyflow:skyflow-java) is on2.x. The two artifacts have separate version lines, so a lowerflowvaultversion number does not mean it is older or behind. Upgrade each artifact on its own; the current release is listed on Maven Central.
- Table of Contents
- Overview
- Install
- Quickstart
- Authenticate
- Initialize the client
- VaultController — Bulk operations
- VaultController — Unary operations
- SDK Guidelines: Unary vs Bulk Operations
- Bulk Insert
- Bulk Tokenize
- Bulk Detokenize
- Bulk Delete Tokens
- Insert
- Detokenize
- Get
- Update
- Delete
- Custom Request Headers
- Error Handling
- Authenticate using a Skyflow service account, an API key, or a bearer token — see Authenticate.
- Perform bulk Vault API operations — insert, tokenize, detokenize, and delete tokens — each with a synchronous and an async variant, built for high-throughput Flow Vault workloads.
- Perform unary Vault API operations — insert, detokenize, get, update, and delete — a single API call each, for when you want a plain request and response rather than the bulk batching machinery. See VaultController — Unary operations.
- Per-record reporting, not all-or-nothing. A bulk call succeeds as a call even when individual records fail; every response reports a summary plus the outcome of each individual record or token. See Error Handling.
- Java 8 and above
implementation 'com.skyflow:skyflow-flowvault-java:1.2.0'
<dependency>
<groupId>com.skyflow</groupId>
<artifactId>skyflow-flowvault-java</artifactId>
<version>1.2.0</version>
</dependency>The snippets above pin the latest release at the time of writing. Check Maven Central for the current version and GitHub releases for what changed in each one.
import com.skyflow.Skyflow;
import com.skyflow.config.Credentials;
import com.skyflow.config.VaultConfig;
import com.skyflow.enums.Env;
import com.skyflow.vault.controller.VaultController;
Credentials credentials = new Credentials();
credentials.setApiKey("<API_KEY>"); // or setToken / setCredentialsString / setPath
VaultConfig vaultConfig = new VaultConfig();
vaultConfig.setVaultId("<VAULT_ID>");
vaultConfig.setClusterId("<CLUSTER_ID>"); // part of the vault URL, e.g. https://{clusterId}.vault.skyflowapis.com
vaultConfig.setEnv(Env.PROD);
vaultConfig.setCredentials(credentials);
Skyflow skyflowClient = Skyflow.builder()
.addVaultConfig(vaultConfig)
.build();
// Returns the controller for the first configured vault
VaultController vault = skyflowClient.vault();vault() with no arguments returns the controller for the first vault added to the builder. To talk to more than one vault from a single client, register each with addVaultConfig(...) and fetch each controller by ID: skyflowClient.vault("<VAULT_ID>").
Requests are authorized with Skyflow credentials that you attach to a Credentials object. Credentials comes from the shared common module, so it is the same class skyvault uses.
Set exactly one of the following on a Credentials instance. If you set more than one, the last one set wins.
| Credential | Setter | What it is |
|---|---|---|
| API key | setApiKey(String) |
A long-lived key that authenticates and authorizes requests to the API. Simplest option. |
| Bearer token | setToken(String) |
A short-lived access token, typically generated from service account credentials. See Generate a bearer token. |
| Credentials file path | setPath(String) |
Filesystem path to a service account credentials.json. The SDK generates and refreshes bearer tokens from it. |
| Credentials string | setCredentialsString(String) |
The contents of a service account credentials.json as a JSON string — use this when the credentials come from a secret store rather than a file. |
Two optional modifiers apply when the SDK is generating tokens for you (that is, with setPath or setCredentialsString):
| Setter | Description |
|---|---|
setRoles(ArrayList<String>) |
Restrict the generated token to specific role IDs (a scoped token). |
setContext(String) / setContext(Map<String, Object>) |
Attach context to the generated token for context-aware authorization. |
// API key
Credentials apiKeyCredentials = new Credentials();
apiKeyCredentials.setApiKey("<API_KEY>");
// Bearer token you generated yourself
Credentials tokenCredentials = new Credentials();
tokenCredentials.setToken("<BEARER_TOKEN>");
// Service account credentials file — the SDK handles token generation and refresh
Credentials fileCredentials = new Credentials();
fileCredentials.setPath("<PATH_TO_CREDENTIALS_JSON>");
// Service account credentials as a JSON string
Credentials stringCredentials = new Credentials();
stringCredentials.setCredentialsString("<CREDENTIALS_JSON_STRING>");Credentials resolve most specific first:
- Per-vault —
vaultConfig.setCredentials(credentials). Wins for that vault. - Client-wide —
Skyflow.builder().addSkyflowCredentials(credentials). Used by any vault that has none of its own. - Environment — if neither is provided, the SDK reads the
SKYFLOW_CREDENTIALSenvironment variable.
If none of the three yields credentials, the call fails with a SkyflowException.
If you would rather manage tokens yourself, the service-account utilities ship inside skyflow-flowvault-java. This section covers each of them:
- Generate a bearer token — mint a token from service account credentials. Tokens are valid for 60 minutes and carry the service account's permissions. Use this when you only need basic authentication.
- Generate a bearer token with context — embed context values into the token so vault policies can evaluate them and end-user identity can be tracked. Use this when policies depend on contextual attributes.
- Generate a scoped bearer token — restrict the token to specific roles of a service account that has several. Use this to enforce role-based access per token.
- Generate signed data tokens — sign existing data tokens with the service account's private key so they can only be detokenized by a caller holding the matching context and permissions.
BearerToken generates a bearer token from a credentials JSON file, or from the same JSON passed as a string. getBearerToken() caches the token and only mints a new one once the current one has expired, so it is safe to call per request.
import com.skyflow.errors.SkyflowException;
import com.skyflow.serviceaccount.util.BearerToken;
import com.skyflow.serviceaccount.util.Token;
import java.io.File;
/**
* Example program to generate a Bearer Token using Skyflow's BearerToken utility.
* The token can be generated in two ways:
* 1. Using the file path to a credentials.json file.
* 2. Using the JSON content of the credentials file as a string.
*/
public class BearerTokenGenerationExample {
public static void main(String[] args) {
// Variable to store the generated token
String token = null;
// Example 1: Generate Bearer Token using a credentials.json file
try {
// Specify the full file path to the credentials.json file
String filePath = "<YOUR_CREDENTIALS_FILE_PATH>";
// Check if the token is either not initialized or has expired
if (Token.isExpired(token)) {
// Create a BearerToken object using the credentials file
BearerToken bearerToken = BearerToken.builder()
.setCredentials(new File(filePath)) // Set credentials from the file path
.build();
// Generate a new Bearer Token
token = bearerToken.getBearerToken();
}
// Print the generated Bearer Token to the console
System.out.println("Generated Bearer Token (from file): " + token);
} catch (SkyflowException e) {
// Handle any exceptions encountered during the token generation process
e.printStackTrace();
}
// Example 2: Generate Bearer Token using the credentials JSON as a string
try {
// Provide the credentials JSON content as a string
String fileContents = "<YOUR_CREDENTIALS_FILE_CONTENTS_AS_STRING>";
// Check if the token is either not initialized or has expired
if (Token.isExpired(token)) {
// Create a BearerToken object using the credentials string
BearerToken bearerToken = BearerToken.builder()
.setCredentials(fileContents) // Set credentials from the string
.build();
// Generate a new Bearer Token
token = bearerToken.getBearerToken();
}
// Print the generated Bearer Token to the console
System.out.println("Generated Bearer Token (from string): " + token);
} catch (SkyflowException e) {
// Handle any exceptions encountered during the token generation process
e.printStackTrace();
}
}
}To use the token with the SDK, attach it to Credentials:
Credentials credentials = new Credentials();
credentials.setToken(token);Context-aware authorization embeds context values into a bearer token during its generation so you can reference those values in your policies. This enables more flexible access controls, such as tracking end-user identity when making API calls through a service account, and is required for detokenizing signed data tokens.
A service account with the context_id identifier generates bearer tokens that carry a context_identifier JWT claim. Those tokens are valid for 60 minutes and can call the Data and Management APIs, subject to the service account's permissions.
setCtx() accepts either a String or a Map<String, Object>.
String context — use when your policy references a single context value:
BearerToken token = BearerToken.builder()
.setCredentials(new File(filePath))
.setCtx("user_12345")
.build();JSON object context — use when your policy needs multiple context values for conditional data access. Each key in the Map maps to a Skyflow CEL policy variable under request.context.*:
Map<String, Object> ctx = new HashMap<>();
ctx.put("role", "admin");
ctx.put("department", "finance");
ctx.put("user_id", "user_12345");
BearerToken token = BearerToken.builder()
.setCredentials(new File(filePath))
.setCtx(ctx)
.build();With the map above, your Skyflow policies can reference request.context.role, request.context.department, and request.context.user_id to make conditional access decisions.
Context map keys must contain only alphanumeric characters and underscores ([a-zA-Z0-9_]). An invalid key throws a SkyflowException when the builder runs.
A service account with multiple roles can generate bearer tokens limited to specific roles by passing their role IDs. This is useful for services with several responsibilities, such as separating billing access from analytics access. The generated tokens are valid for 60 minutes and can only execute operations permitted by the designated roles.
import com.skyflow.errors.SkyflowException;
import com.skyflow.serviceaccount.util.BearerToken;
import java.io.File;
import java.util.ArrayList;
/**
* This example demonstrates how to generate a Scoped Bearer Token in two ways:
* 1. Using a credentials file specified by its file path.
* 2. Using the credentials as a string.
* <p>
* Scoped tokens are generated by assigning specific roles for access control.
*/
public class ScopedTokenGenerationExample {
public static void main(String[] args) {
String scopedToken = null; // Variable to store the generated Scoped Bearer Token
// Example 1: Generate Scoped Token using a credentials file path
try {
// Step 1: Specify the roles required for the scoped token
ArrayList<String> roles = new ArrayList<>();
roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID
// Step 2: Specify the path to the credentials file
String filePath = "<YOUR_CREDENTIALS_FILE_PATH>"; // Replace with the actual file path
// Step 3: Create a BearerToken object using the file path and roles
BearerToken bearerToken = BearerToken.builder()
.setCredentials(new File(filePath)) // Provide the credentials file
.setRoles(roles) // Set the roles for the scoped token
.build();
// Step 4: Generate and print the Scoped Bearer Token
scopedToken = bearerToken.getBearerToken();
System.out.println("Scoped Token (using file path): " + scopedToken);
} catch (SkyflowException e) { // Handle exceptions during token generation
System.out.println("Error occurred while generating Scoped Token using file path:");
e.printStackTrace();
}
// Example 2: Generate Scoped Token using credentials as a string
try {
// Step 1: Specify the roles required for the scoped token
ArrayList<String> roles = new ArrayList<>();
roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID
// Step 2: Specify the credentials as a string (file contents)
String fileContents = "<YOUR_CREDENTIALS_FILE_CONTENTS_AS_STRING>"; // Replace with actual file contents
// Step 3: Create a BearerToken object using the credentials string and roles
BearerToken bearerToken = BearerToken.builder()
.setCredentials(fileContents) // Provide the credentials as a string
.setRoles(roles) // Set the roles for the scoped token
.build();
// Step 4: Generate and print the Scoped Bearer Token
scopedToken = bearerToken.getBearerToken();
System.out.println("Scoped Token (using credentials string): " + scopedToken);
} catch (SkyflowException e) { // Handle exceptions during token generation
System.out.println("Error occurred while generating Scoped Token using credentials string:");
e.printStackTrace();
}
}
}Notes:
setCredentialsaccepts either the path to a service account credentials file or the credentials JSON as a string.- If both a file and a string are provided, the last call wins.
- To generate bearer tokens concurrently from several threads, see BearerTokenGenerationUsingThreadsExample.
Skyflow generates data tokens when sensitive data is inserted into the vault. Those data tokens can be digitally signed with the private key of the service account credentials, which adds a further layer of protection. A signed token can only be detokenized by passing it together with a bearer token generated from service account credentials that hold the matching context and permissions.
setCtx() on SignedDataTokens.builder() accepts either a String or a Map<String, Object>, in the same format as bearer tokens:
import com.skyflow.serviceaccount.util.SignedDataTokenResponse;
import com.skyflow.serviceaccount.util.SignedDataTokens;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
ArrayList<String> dataTokens = new ArrayList<>();
dataTokens.add("<YOUR_DATA_TOKEN_1>");
// String context
SignedDataTokens signedToken = SignedDataTokens.builder()
.setCredentials(new File(filePath))
.setCtx("user_12345")
.setTimeToLive(30) // seconds
.setDataTokens(dataTokens)
.build();
// JSON object context
Map<String, Object> ctx = new HashMap<>();
ctx.put("role", "analyst");
ctx.put("department", "research");
SignedDataTokens signedTokenWithMap = SignedDataTokens.builder()
.setCredentials(new File(filePath))
.setCtx(ctx)
.setTimeToLive(30)
.setDataTokens(dataTokens)
.build();
List<SignedDataTokenResponse> signed = signedToken.getSignedDataTokens();
for (SignedDataTokenResponse entry : signed) {
System.out.println(entry.getToken() + " -> " + entry.getSignedToken());
}Response:
[
{
"dataToken": "5530-4316-0674-5748",
"signedDataToken": "signed_token_eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzLCpZjA"
}
]Notes:
setCredentialsaccepts either the path to a service account credentials file or the credentials JSON as a string. If both are provided, the last call wins.setTimeToLiveis in seconds and defaults to 60.
Skyflow is the client. Build it once, keep it for the lifetime of your application, and get a VaultController from it with vault().
import com.skyflow.Skyflow;
import com.skyflow.config.Credentials;
import com.skyflow.config.VaultConfig;
import com.skyflow.enums.Env;
import com.skyflow.enums.LogLevel;
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
public class InitFlowVaultClient {
public static void main(String[] args) throws SkyflowException {
// Step 1: Credentials — exactly one credential type
Credentials credentials = new Credentials();
credentials.setPath("<PATH_TO_CREDENTIALS_JSON>");
// Step 2: Vault configuration
VaultConfig vaultConfig = new VaultConfig();
vaultConfig.setVaultId("<VAULT_ID>");
vaultConfig.setClusterId("<CLUSTER_ID>");
vaultConfig.setEnv(Env.PROD); // DEV, STAGE, SANDBOX, or PROD (default)
vaultConfig.setCredentials(credentials);
// Optional: vault-level HTTP overrides
vaultConfig.setTimeout(120); // overall call timeout, in seconds
vaultConfig.setMaxRetries(2); // retries after the first failure
// Step 3: Build the client
Skyflow skyflowClient = Skyflow.builder()
.setLogLevel(LogLevel.INFO) // default is ERROR
.addVaultConfig(vaultConfig)
.build();
// Step 4: Get the controller and issue bulk calls
VaultController vault = skyflowClient.vault();
}
}| Setter | Type | Description |
|---|---|---|
setVaultId(String) |
required | The vault's ID. |
setClusterId(String) |
required | The cluster portion of the vault URL — https://{clusterId}.vault.skyflowapis.com. |
setEnv(Env) |
optional | Env.DEV, Env.STAGE, Env.SANDBOX, or Env.PROD. Defaults to PROD; passing null also resolves to PROD. |
setCredentials(Credentials) |
optional | Credentials for this vault. Falls back to client-wide credentials, then SKYFLOW_CREDENTIALS. |
setVaultUrl(String) |
optional | Full vault URL, when it cannot be derived from clusterId and env. |
setTimeout(Integer) |
optional | Overall call timeout in seconds, including retries. |
setConnectTimeout(Integer) |
optional | Per-attempt connection timeout, in seconds. |
setReadTimeout(Integer) |
optional | Per-attempt response-read timeout, in seconds. |
setWriteTimeout(Integer) |
optional | Per-attempt request-write timeout, in seconds. |
setMaxRetries(Integer) |
optional | Retry attempts after the first failure. |
setInitialRetryDelayMillis(Long) |
optional | Backoff before the first retry, in milliseconds. |
setMaxRetryDelayMillis(Long) |
optional | Ceiling the exponential backoff grows to, in milliseconds. |
| Method | Description |
|---|---|
addVaultConfig(VaultConfig) |
Register a vault. The first one registered is what vault() returns. |
updateVaultConfig(VaultConfig) |
Update a registered vault in place. null fields mean "leave as is". |
removeVaultConfig(String vaultId) |
Unregister a vault. |
addSkyflowCredentials(Credentials) |
Client-wide credentials for vaults that don't set their own. |
setLogLevel(LogLevel) |
DEBUG, INFO, WARN, ERROR (default), or OFF. |
timeout(int) / connectTimeout(int) / readTimeout(int) / writeTimeout(int) |
Client-wide HTTP timeouts, in seconds. |
maxRetries(int) / initialRetryDelayMillis(long) / maxRetryDelayMillis(long) |
Client-wide retry policy. |
build() |
Produce the Skyflow client. |
Every method throws SkyflowException on validation errors and returns the builder for chaining.
Once built, skyflowClient.vault() returns the first registered vault's controller; skyflowClient.vault("<VAULT_ID>") returns the controller for a specific registered vault, which is how one client talks to more than one vault.
Each HTTP setting resolves most specific first: the value on VaultConfig, else the client-wide value on Skyflow.builder(), else the SDK default. Only null means "inherit" — an explicit 0 is a real value and overrides the level below it.
| Setting | SDK default |
|---|---|
timeout (overall call, incl. retries) |
60 s |
connectTimeout / readTimeout / writeTimeout (per attempt) |
10 s (underlying HTTP client default) |
maxRetries |
0 — retries are opt-in, so non-idempotent bulk writes are never replayed silently |
initialRetryDelayMillis |
500 ms |
maxRetryDelayMillis |
2000 ms |
// Client-wide policy, overridden for one vault
VaultConfig vaultConfig = new VaultConfig();
vaultConfig.setVaultId("<VAULT_ID>");
vaultConfig.setClusterId("<CLUSTER_ID>");
vaultConfig.setCredentials(credentials);
vaultConfig.setTimeout(300); // this vault gets 300s...
Skyflow skyflowClient = Skyflow.builder()
.timeout(60) // ...instead of the client-wide 60s
.maxRetries(3) // this vault inherits 3 retries
.initialRetryDelayMillis(500L)
.maxRetryDelayMillis(4000L)
.addVaultConfig(vaultConfig)
.build();The SDK logs through java.util.logging at LogLevel.ERROR by default. Levels rank DEBUG < INFO < WARN < ERROR < OFF; setting a level prints that level and everything above it. Change it with Skyflow.builder().setLogLevel(LogLevel.DEBUG).
- Thread safety —
Skyflowand everyVaultControllerit hands out are safe to share and call concurrently across threads, which is exactly how Initialize the client expects them to be used: build once, reuse for the app's lifetime. - Async execution — each
*Asynccall runs its batches on a small dedicated thread pool sized to that call's concurrency limit (see Batching and concurrency), scoped to the call rather than shared globally.
VaultController is returned by skyflowClient.vault(). flowvault exposes these bulk vault operations:
| Method | Parameters | Returns | Description |
|---|---|---|---|
bulkInsert(BulkInsertRequest) |
BulkInsertRequest, optional BulkInsertOptions |
BulkInsertResponse |
Insert many records, optionally across multiple tables, in one call |
bulkInsertAsync(BulkInsertRequest) |
same | CompletableFuture<BulkInsertResponse> |
Async variant of bulkInsert |
bulkTokenize(BulkTokenizeRequest) |
BulkTokenizeRequest, optional BulkTokenizeOptions |
BulkTokenizeResponse |
Tokenize many values, each against one or more named token groups |
bulkTokenizeAsync(BulkTokenizeRequest) |
same | CompletableFuture<BulkTokenizeResponse> |
Async variant of bulkTokenize |
bulkDetokenize(BulkDetokenizeRequest) |
BulkDetokenizeRequest, optional BulkDetokenizeOptions |
BulkDetokenizeResponse |
Detokenize many tokens, optionally with a redaction override per token group |
bulkDetokenizeAsync(BulkDetokenizeRequest) |
same | CompletableFuture<BulkDetokenizeResponse> |
Async variant of bulkDetokenize |
bulkDeleteTokens(BulkDeleteTokensRequest) |
BulkDeleteTokensRequest, optional BulkDeleteTokensOptions |
BulkDeleteTokensResponse |
Delete many tokens in one call |
bulkDeleteTokensAsync(BulkDeleteTokensRequest) |
same | CompletableFuture<BulkDeleteTokensResponse> |
Async variant of bulkDeleteTokens |
Which of these operations makes sense depends on whether the vault is structured (has a schema — tables and columns) or schemaless (stores standalone tokens with no table structure):
| Operation | Supported on |
|---|---|
bulkInsert / bulkInsertAsync |
Structured (schema) vaults — inserts into a table's columns. |
bulkTokenize / bulkTokenizeAsync |
Schemaless vaults — tokenizes a raw value directly against named token groups, with no table involved. |
bulkDeleteTokens / bulkDeleteTokensAsync |
Schemaless vaults. |
bulkDetokenize / bulkDetokenizeAsync |
Both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. |
This reflects supported use cases, not something the SDK validates or blocks — nothing stops you from calling, say, bulkTokenize against a structured vault; it just isn't the intended usage and isn't a scenario the SDK is tested against.
Each method also accepts an optional options object (BulkInsertOptions, BulkTokenizeOptions, BulkDetokenizeOptions, BulkDeleteTokensOptions) — see Custom Request Headers.
A single bulk call accepts at most 100,000 records or tokens; anything larger is rejected up front with a SkyflowException. Under that ceiling the SDK splits the payload into batches and sends them concurrently, which is why errors from one call can carry different requestId values.
Every bulk response has the same two-part shape:
- a summary — totals for the call (e.g.
totalRecords/totalInserted/totalFailedfor insert) - a records list — one entry per submitted record or token, in input order, each carrying its own
index,httpCode, anderror
That per-record shape is the point of these APIs; see Error Handling for the full model.
Batch size and concurrency are configured per operation through environment variables — there is no builder or options API for them. Each value is read from the process environment first, then from a .env file in the working directory.
| Operation | Batch size variable | Default | Max | Concurrency variable | Default | Max |
|---|---|---|---|---|---|---|
| Bulk insert | INSERT_BATCH_SIZE |
50 | 1000 | INSERT_CONCURRENCY_LIMIT |
1 | 100 |
| Bulk tokenize | TOKENIZE_BATCH_SIZE |
50 | 1000 | TOKENIZE_CONCURRENCY_LIMIT |
1 | 100 |
| Bulk detokenize | DETOKENIZE_BATCH_SIZE |
50 | 1000 | DETOKENIZE_CONCURRENCY_LIMIT |
1 | 100 |
| Bulk delete tokens | DELETE_TOKENS_BATCH_SIZE |
50 | 1000 | DELETE_TOKENS_CONCURRENCY_LIMIT |
1 | 100 |
Concurrency defaults to 1, so batches are sent one after another unless you raise the limit.
How each value is resolved:
- Batch size —
min(yourValue, max). Above the max, the SDK logs a warning and uses the max. Zero, negative, or non-numeric values log a warning and fall back to the default. - Concurrency —
min(yourValue, max, batchCount), wherebatchCount = ceil(itemCount / batchSize). Concurrency never exceeds the number of batches there are to run. Same warning-and-fallback behaviour for invalid values.
Those warnings are emitted at WARN, which the default ERROR level hides — set LogLevel.WARN or below to see them (see Logging).
For example, 500 records with INSERT_BATCH_SIZE=100 and INSERT_CONCURRENCY_LIMIT=10 produces 5 batches, all 5 in flight at once — the concurrency is capped to 5, not 10.
# .env
INSERT_BATCH_SIZE=100
INSERT_CONCURRENCY_LIMIT=5The 100,000-item ceiling per bulk call is a separate, fixed limit and is not configurable.
Alongside the bulk methods, VaultController exposes five unary operations. Each sends exactly one API call and hands the result straight back:
| Method | Parameters | Returns | Description |
|---|---|---|---|
insert(InsertRequest) |
InsertRequest, optional InsertOptions |
InsertResponse |
Insert records, optionally across multiple tables, in one call |
detokenize(DetokenizeRequest) |
DetokenizeRequest, optional DetokenizeOptions |
DetokenizeResponse |
Detokenize tokens, optionally with a redaction override per token group |
get(GetRequest) |
GetRequest, optional GetOptions |
GetResponse |
Read records by skyflow ID or unique value, optionally with a redaction override per column |
update(UpdateRequest) |
UpdateRequest, optional UpdateOptions |
UpdateResponse |
Update records by skyflow ID |
delete(DeleteRequest) |
DeleteRequest, optional DeleteOptions |
DeleteResponse |
Delete records by skyflow ID or unique value |
insert and detokenize are the unary counterparts of bulkInsert and bulkDetokenize — the same request builders, sent as one call instead of many batches. get, update, and delete have no bulk counterpart at all; they exist only in this unary form.
Each method also accepts an optional options object (InsertOptions, DetokenizeOptions, GetOptions, UpdateOptions, DeleteOptions) — see Custom Request Headers.
Everything the bulk machinery adds — batching, concurrency, the payload ceiling, the summary, the per-item index — is absent here. What survives is the per-record reporting:
| Bulk operations | Unary operations | |
|---|---|---|
| Async variant | Yes — bulkInsertAsync, and so on |
No. Wrap the call yourself if you need one |
| Batching and concurrency | Configured per operation — see Batching and concurrency | Not applicable — one payload, one call |
| Payload ceiling | 100,000 records or tokens per call | Not enforced by the SDK; the vault's own request limits still apply |
| Response summary | getSummary() |
None — read the records list |
Per-item getIndex() / getRequestId() |
Yes | No. Records come back in submitted order, and the x-request-id of the single call reaches you only through a thrown SkyflowException |
| Retry helper | getRecordsToRetry() / getTokensToRetry() |
None — filter the records yourself, see Retrying the failed records |
Per-item getHttpCode() / getError() |
Yes | Yes, on every unary operation |
The same distinction as Schema vs. schemaless vaults applies. Four of the five unary operations address records inside a table, so they only make sense against a structured vault:
| Operation | Supported on |
|---|---|
insert |
Structured (schema) vaults — inserts into a table's columns. |
get |
Structured vaults — reads a table's records by skyflow ID or unique value. |
update |
Structured vaults — updates a table's records by skyflow ID. |
delete |
Structured vaults — deletes a table's records. Distinct from bulkDeleteTokens, which removes tokens only and leaves the record in place. |
detokenize |
Both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. |
Both Unary and Bulk operations accept as many records as you pass. The key difference is how the SDK makes HTTP calls and manages concurrency.
- Makes exactly one HTTP call per SDK invocation, regardless of the number of records.
- The application is responsible for any chunking, batching, and concurrency.
- Best suited for:
- Single-event or low-volume ingestion
- Interactive or user-facing requests where immediate results are required
- Applications that already have their own concurrency or job-management mechanism
Use Unary when you want the application to control request execution.
- The SDK automatically splits records into
batchSize-sized chunks. - It dispatches up to
concurrencyLimitbatches in parallel. - The SDK therefore owns batching, parallel dispatch, and request coordination.
- Best suited for:
- Large datasets
- Imports and backfills
- ETL and data migration workloads
- Bulk/streaming ingestion where you want the SDK to manage batching and concurrency
Use Bulk when you want the SDK to optimize request execution for high-volume workloads.
A bulk call sent with fewer records than batchSize (default 50) still produces exactly one batch — concurrency resolves to 1 regardless of ..._CONCURRENCY_LIMIT — so there's no batching benefit, only the overhead of the bulk machinery on top. Use unary instead for calls at that size.
For Bulk operations, choose concurrencyLimit based on the available CPU and the ratio of task wait time to compute time:
concurrency ≈ N_cpu × U_cpu × (1 + W/C)
Where:
N_cpu= number of CPU cores available to the processU_cpu= target CPU utilization, between 0 and 1W= wait time / API latency per callC= compute time per call — approximately 5 ms for the SDK
- VUs ≤ 20: a single CPU core is generally sufficient.
- VUs > 20: consider increasing CPU capacity and tune concurrency accordingly.
- For higher-throughput workloads, dual- or quad-core configurations are a good starting point.
- Start with the formula as a baseline and benchmark with your actual API latency and workload before increasing concurrency further.
Insert many records — even across different tables — in a single call. Each record is a BulkInsertRequestRecord with its own data and, optionally, its own tableName and upsert.
Vault type supported: structured (schema) vaults. See Schema vs. schemaless vaults.
Note:
tableNamemust be specified at exactly one level: either on the request (BulkInsertRequest.builder().tableName(...)) or on every record (BulkInsertRequestRecord.builder().tableName(...)) — not both, and not neither.upsertis optional, but wherever you supply it, it must sit at the same level astableName. Request-leveltableNamepairs with request-levelupsert; record-leveltableNamepairs with per-recordupsert.UpsertOptionsrequiresuniqueColumns.updateTypeaccepts"UPDATE"or"REPLACE"— if omitted, the SDK sends noupdateTypeat all, and the vault treats that the same as"UPDATE".
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.BulkInsertRequest;
import com.skyflow.vault.data.BulkInsertRequestRecord;
import com.skyflow.vault.data.BulkInsertResponse;
import com.skyflow.vault.data.InsertRequestRecord;
import com.skyflow.vault.data.UpsertOptions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class BulkInsertExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
// Step 1: Build each record. Here tableName lives on the records, so each one carries it.
Map<String, Object> record1Data = new HashMap<>();
record1Data.put("card_number", "4111111111111111");
record1Data.put("cardholder_name", "john doe");
BulkInsertRequestRecord record1 = BulkInsertRequestRecord.builder()
.tableName("table1")
.data(record1Data)
.build();
Map<String, Object> record2Data = new HashMap<>();
record2Data.put("email", "jane.doe@example.com");
BulkInsertRequestRecord record2 = BulkInsertRequestRecord.builder()
.tableName("table2")
.data(record2Data)
// upsert sits at the record level here, matching where tableName sits
.upsert(UpsertOptions.builder()
.uniqueColumns(Arrays.asList("email"))
.updateType("UPDATE")
.build())
.build();
List<InsertRequestRecord> records = new ArrayList<>();
records.add(record1);
records.add(record2);
// Step 2: Build the BulkInsertRequest
BulkInsertRequest insertRequest = BulkInsertRequest.builder()
.records(records)
.build();
// Step 3: Perform the bulk insert
BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest);
System.out.println(insertResponse);
}
}To put the table name on the request instead, drop tableName from every record and build the request as:
BulkInsertRequest insertRequest = BulkInsertRequest.builder()
.tableName("table1")
.upsert(UpsertOptions.builder().uniqueColumns(Arrays.asList("email")).build())
.records(records)
.build();import java.util.concurrent.CompletableFuture;
VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
CompletableFuture<BulkInsertResponse> future = vault.bulkInsertAsync(insertRequest);
future.thenAccept(response -> System.out.println(response));Sample response:
{
"summary": { "totalRecords": 2, "totalInserted": 1, "totalFailed": 1 },
"records": [
{
"index": 0,
"requestId": null,
"tableName": "table1",
"skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
"tokens": {
"card_number": [
{ "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" }
],
"cardholder_name": [
{ "token": "b2308e2a-c1f5-469b-97b7-1f193159399b", "tokenGroupName": "deterministic_string" },
{ "token": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "tokenGroupName": "vault_token_group" }
]
},
"data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" },
"hashedData": { "card_number": "b6e6d...c3f9" },
"httpCode": 200,
"error": null
},
{
"index": 1,
"requestId": "a1b2c3d4-...",
"tableName": "table2",
"skyflowId": null,
"tokens": null,
"data": null,
"hashedData": null,
"httpCode": 400,
"error": "Insert failed. Column email is invalid."
}
]
}getTokens() returns Map<String, List<Token>> — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. On the wire the API models this generically (Object, not a fixed type) to stay flexible, but the SDK parses it into Token objects before handing it back, so callers get Token.getToken()/Token.getTokenGroupName() directly with no casting required:
for (BulkInsertResponseRecord record : insertResponse.getRecords()) {
if (record.getTokens() != null) {
for (Token token : record.getTokens().get("card_number")) {
System.out.println(token.getTokenGroupName() + " -> " + token.getToken());
}
}
}The parser (Token.parseTokens()) normalizes every shape the raw wire value is known to take — a list of {token, tokenGroupName} entries, a single such entry not wrapped in a list, or a bare token value with no group information — into a consistent List<Token>, rather than throwing on an unexpected one. getTokens() returns null when the record has no tokens (e.g. a failed record).
For a structured column (e.g. an object or array value), each entry also carries getPath() — the location within that column's own value the token came from, such as "street" or "phone_numbers[0].type". It's null for a flat column, where there's nothing to point into.
Accessors: insertResponse.getSummary(), insertResponse.getRecords(), and on each record getIndex(), getTableName(), getSkyflowId(), getTokens(), getData(), getHashedData(), getHttpCode(), getError(), getRequestId().
Deprecation notice:
getFields()is deprecated in favor ofgetTokens()— it is kept only for backward compatibility and will be removed in a future release. Update call sites togetTokens().
Use insertResponse.getRecordsToRetry() to get back only the BulkInsertRequestRecords worth resubmitting — see Retrying the failed records.
Tokenize many values in one call. Each value can be tokenized against one or more named token groups.
Vault type supported: schemaless vaults. See Schema vs. schemaless vaults.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.BulkTokenizeRequest;
import com.skyflow.vault.data.BulkTokenizeRequestRecord;
import com.skyflow.vault.data.BulkTokenizeResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BulkTokenizeExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
BulkTokenizeRequestRecord record1 = BulkTokenizeRequestRecord.builder()
.value("4111111111111111")
.tokenGroupNames(Arrays.asList("card_number_cg"))
.build();
BulkTokenizeRequestRecord record2 = BulkTokenizeRequestRecord.builder()
.value("john.doe@example.com")
.tokenGroupNames(Arrays.asList("email_cg"))
.build();
List<BulkTokenizeRequestRecord> records = new ArrayList<>();
records.add(record1);
records.add(record2);
BulkTokenizeRequest tokenizeRequest = BulkTokenizeRequest.builder()
.records(records)
.build();
BulkTokenizeResponse tokenizeResponse = vault.bulkTokenize(tokenizeRequest);
System.out.println(tokenizeResponse);
}
}BulkTokenizeRequestRecord.builder() also accepts token(Object) to supply your own token for the value instead of having the vault generate one.
VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
CompletableFuture<BulkTokenizeResponse> future = vault.bulkTokenizeAsync(tokenizeRequest);Sample response:
{
"summary": { "totalTokens": 2, "totalTokenized": 1, "totalPartial": 0, "totalFailed": 1 },
"records": [
{ "index": 0, "value": "4111111111111111", "tokenGroupName": "card_number_cg", "token": "5479-4229-4622-1393", "httpCode": 200, "error": null, "requestId": null },
{ "index": 1, "value": "john.doe@example.com", "tokenGroupName": "email_cg", "token": null, "httpCode": 400, "error": "Token group email_cg not found.", "requestId": "a1b2c3d4-..." }
]
}records is flat: one entry per (value, token group) outcome, matching the API's own response shape. Because a single value can map to several token groups, several entries can share the same index — that's how the SDK tells you which input value an entry belongs to. The summary classifies by index rather than by entry: fully tokenized values (totalTokenized), partially tokenized values where some groups succeeded and others failed (totalPartial), and fully failed values (totalFailed). The three always add up to totalTokens, which counts input values, not entries.
for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getValue() + " -> " + record.getTokenGroupName() + " = " + record.getToken());
} else {
System.out.println(record.getValue() + " -> " + record.getTokenGroupName() + " failed: " + record.getError());
}
}Detokenize many tokens in one call, optionally overriding the redaction applied per token group via tokenGroupRedactions.
Vault type supported: both. See Schema vs. schemaless vaults.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.BulkDetokenizeRequest;
import com.skyflow.vault.data.BulkDetokenizeResponse;
import com.skyflow.vault.data.TokenGroupRedactions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BulkDetokenizeExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
List<String> tokens = new ArrayList<>(Arrays.asList(
"5479-4229-4622-1393",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
));
TokenGroupRedactions redaction = TokenGroupRedactions.builder()
.tokenGroupName("card_number_cg")
.redaction("MASKED")
.build();
BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder()
.tokens(tokens)
.tokenGroupRedactions(Arrays.asList(redaction))
.build();
BulkDetokenizeResponse detokenizeResponse = vault.bulkDetokenize(detokenizeRequest);
System.out.println(detokenizeResponse);
}
}VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
CompletableFuture<BulkDetokenizeResponse> future = vault.bulkDetokenizeAsync(detokenizeRequest);Sample response:
{
"summary": { "totalTokens": 2, "totalDetokenized": 1, "totalFailed": 1 },
"records": [
{
"index": 0,
"requestId": null,
"value": "4111111111111111",
"tokenGroupName": "card_number_cg",
"metadata": { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "tableName": "table1" },
"httpCode": 200,
"token": "5479-4229-4622-1393",
"error": null
},
{
"index": 1,
"requestId": "a1b2c3d4-...",
"value": null,
"tokenGroupName": null,
"metadata": null,
"httpCode": 404,
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"error": "Token Not Found"
}
]
}record.getMetadata() is typed as a DetokenizeResponseRecordMetadata with getSkyflowId()/getTableName() — no casting into the raw map required (null on records that errored, same as above):
for (BulkDetokenizeResponseRecord record : detokenizeResponse.getRecords()) {
if (record.getMetadata() != null) {
System.out.println(record.getMetadata().getSkyflowId() + " / " + record.getMetadata().getTableName());
}
}Use detokenizeResponse.getTokensToRetry() to get back only the tokens worth resubmitting.
Delete many tokens in one call.
Vault type supported: schemaless vaults. See Schema vs. schemaless vaults.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.BulkDeleteTokensRequest;
import com.skyflow.vault.data.BulkDeleteTokensResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BulkDeleteTokensExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
List<String> tokens = new ArrayList<>(Arrays.asList(
"5479-4229-4622-1393",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
));
BulkDeleteTokensRequest deleteTokensRequest = BulkDeleteTokensRequest.builder()
.tokens(tokens)
.build();
BulkDeleteTokensResponse deleteTokensResponse = vault.bulkDeleteTokens(deleteTokensRequest);
System.out.println(deleteTokensResponse);
}
}VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
CompletableFuture<BulkDeleteTokensResponse> future = vault.bulkDeleteTokensAsync(deleteTokensRequest);Sample response:
{
"summary": { "totalTokens": 2, "totalDeleted": 2, "totalFailed": 0 },
"records": [
{ "index": 0, "token": "5479-4229-4622-1393", "httpCode": 200, "error": null, "requestId": null },
{ "index": 1, "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "httpCode": 200, "error": null, "requestId": null }
]
}for (BulkDeleteTokensResponseRecord record : deleteTokensResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getToken() + " deleted");
} else {
System.out.println(record.getToken() + " failed (" + record.getHttpCode() + "): " + record.getError());
}
}Use deleteTokensResponse.getTokensToRetry() to get back only the tokens worth resubmitting.
Insert records in a single API call — the unary counterpart of Bulk Insert, with no batching or concurrency involved. Each record is an InsertRequestRecord with its own data and, optionally, its own tableName, tokens, and upsert.
Vault type supported: structured (schema) vaults. See Vault type support.
Note:
tableNamemust be specified at exactly one level: either on the request (InsertRequest.builder().tableName(...)) or on every record (InsertRequestRecord.builder().tableName(...)) — not both, and not neither. Same rule as bulk insert.upsertis optional, but wherever you supply it, it must sit at the same level astableName.tokensis optional; when supplied, the map must not be empty and no key or value may be blank.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.InsertRequest;
import com.skyflow.vault.data.InsertRequestRecord;
import com.skyflow.vault.data.InsertResponse;
import com.skyflow.vault.data.InsertResponseRecord;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InsertExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
// Step 1: Build each record. Here tableName lives on the request, so no record carries it.
Map<String, Object> recordData = new HashMap<>();
recordData.put("card_number", "4111111111111111");
recordData.put("cardholder_name", "john doe");
InsertRequestRecord record = InsertRequestRecord.builder()
.data(recordData)
.build();
List<InsertRequestRecord> records = new ArrayList<>();
records.add(record);
// Step 2: Build the InsertRequest
InsertRequest insertRequest = InsertRequest.builder()
.tableName("table1")
.records(records)
.build();
// Step 3: Perform the insert
InsertResponse insertResponse = vault.insert(insertRequest);
System.out.println(insertResponse);
}
}To put the table name on the records instead, drop tableName from the request and set it — along with any upsert — on every InsertRequestRecord, exactly as Bulk Insert shows.
There is no async variant: insert returns its InsertResponse directly.
Sample response:
{
"records": [
{
"tableName": "table1",
"skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
"tokens": {
"card_number": [
{ "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" }
],
"cardholder_name": [
{ "token": "b2308e2a-c1f5-469b-97b7-1f193159399b", "tokenGroupName": "deterministic_string" }
]
},
"data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" },
"hashedData": { "card_number": "b6e6d...c3f9" },
"httpCode": 200,
"error": null,
"requestId": null
}
]
}There is no summary and no per-record index — the records come back in the order you submitted them. requestId behaves exactly as it does on a bulk record: null on success, the failing call's x-request-id on error. getTokens() returns the same parsed Map<String, List<Token>> described under Bulk Insert, so Token.getToken(), Token.getTokenGroupName(), and Token.getPath() are available with no casting.
Accessors: insertResponse.getRecords(), and on each record getTableName(), getSkyflowId(), getTokens(), getData(), getHashedData(), getHttpCode(), getError(), getRequestId().
Deprecation notice:
InsertResponseRecord.getFields()is deprecated in favor ofgetTokens()here too — it is kept only for backward compatibility and will be removed in a future release.
for (InsertResponseRecord record : insertResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getTableName() + " -> " + record.getSkyflowId());
} else {
System.err.println("insert failed [" + record.getHttpCode() + "] " + record.getError());
}
}Detokenize tokens in a single API call — the unary counterpart of Bulk Detokenize, optionally overriding the redaction applied per token group via tokenGroupRedactions.
Vault type supported: both. See Vault type support.
Note:
tokensis required and must not be empty, and no entry may be blank.tokenGroupRedactionsis optional; when supplied, each entry needs a non-blanktokenGroupNameandredaction.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.DetokenizeRequest;
import com.skyflow.vault.data.DetokenizeResponse;
import com.skyflow.vault.data.DetokenizeResponseRecord;
import com.skyflow.vault.data.TokenGroupRedactions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DetokenizeExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
List<String> tokens = new ArrayList<>(Arrays.asList(
"5479-4229-4622-1393",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
));
TokenGroupRedactions redaction = TokenGroupRedactions.builder()
.tokenGroupName("card_number_cg")
.redaction("MASKED")
.build();
DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder()
.tokens(tokens)
.tokenGroupRedactions(Arrays.asList(redaction))
.build();
DetokenizeResponse detokenizeResponse = vault.detokenize(detokenizeRequest);
System.out.println(detokenizeResponse);
}
}There is no async variant: detokenize returns its DetokenizeResponse directly.
Sample response:
{
"records": [
{
"token": "5479-4229-4622-1393",
"value": "4111111111111111",
"tokenGroupName": "card_number_cg",
"metadata": { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "tableName": "table1" },
"httpCode": 200,
"error": null,
"requestId": null
},
{
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"value": null,
"tokenGroupName": null,
"metadata": null,
"httpCode": 404,
"error": "Token Not Found",
"requestId": "a1b2c3d4-..."
}
]
}Same per-record shape as bulk detokenize, minus index — requestId behaves the same on both: null on success, the failing call's x-request-id on error. record.getMetadata() is a typed DetokenizeResponseRecordMetadata with getSkyflowId()/getTableName() — null on records that errored:
for (DetokenizeResponseRecord record : detokenizeResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getToken() + " -> " + record.getValue()
+ " (" + record.getTokenGroupName() + ")");
} else {
System.err.println(record.getToken() + " failed ["
+ record.getHttpCode() + "] " + record.getError());
}
}Accessors: detokenizeResponse.getRecords(), and on each record getToken(), getValue(), getTokenGroupName(), getMetadata(), getHttpCode(), getError(), getRequestId(). getValue() is typed Object, passed straight through from the API.
Read records back from a table, by skyflow ID or by unique value, optionally overriding the redaction applied per column via columnRedactions.
Vault type supported: structured (schema) vaults. See Vault type support.
Note:
- A
GetRequestworks in one of two modes, and they are mutually exclusive: single-table (tableName,skyflowIds/uniqueValues,columns,columnRedactions,limit,offset) or multi-table (records, a list ofGetRequestRecord). Setting fields from both modes fails validation. tableNameis required, and exactly one ofskyflowIdsoruniqueValuesmust be supplied — both, or neither, fails validation. This holds per record in multi-table mode.uniqueValuesis aList<Map<String, Object>>: one map per record, each holding the unique column-name/value pairs that identify it.columnsselects the columns to return; omit it for all of them. When supplied, it must be non-empty with no blank entries.limitandoffsetapply to the call as a whole and are only sent in single-table mode — aGetRequestRecordhas nolimit/offsetof its own, and values set on a multi-table request are not sent.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.ColumnRedactions;
import com.skyflow.vault.data.GetRequest;
import com.skyflow.vault.data.GetResponse;
import com.skyflow.vault.data.GetResponseRecord;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public class GetExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
// Step 1: Optionally override how individual columns come back. Anything not listed
// uses the redaction configured on the vault's policy.
ColumnRedactions redaction = ColumnRedactions.builder()
.columnName("card_number")
.redaction("MASKED")
.build();
// Step 2: Build the GetRequest — single-table mode, selecting records by skyflow ID
GetRequest getRequest = GetRequest.builder()
.tableName("table1")
.skyflowIds(new ArrayList<>(Arrays.asList(
"9fac9201-7b8a-4446-93f8-5244e1213bd1",
"b2308e2a-c1f5-469b-97b7-1f193159399b")))
.columns(new ArrayList<>(Arrays.asList("card_number", "cardholder_name")))
.columnRedactions(Collections.singletonList(redaction))
.limit(10)
.offset(0)
.build();
// Step 3: Perform the get
GetResponse getResponse = vault.get(getRequest);
System.out.println(getResponse);
}
}To select records by unique value instead of skyflow ID, swap skyflowIds(...) for uniqueValues(...):
Map<String, Object> uniqueValue = new HashMap<>();
uniqueValue.put("email", "jane.doe@example.com");
GetRequest getRequest = GetRequest.builder()
.tableName("table2")
.uniqueValues(Collections.singletonList(uniqueValue))
.build();To read from more than one table in a single call, use multi-table mode — each GetRequestRecord carries its own table and lookup fields, and none of the single-table fields may be set on the request itself:
GetRequestRecord fromTable1 = GetRequestRecord.builder()
.tableName("table1")
.skyflowIds(Arrays.asList("9fac9201-7b8a-4446-93f8-5244e1213bd1"))
.columns(Arrays.asList("card_number"))
.build();
GetRequestRecord fromTable2 = GetRequestRecord.builder()
.tableName("table2")
.uniqueValues(Collections.singletonList(uniqueValue))
.build();
GetRequest getRequest = GetRequest.builder()
.records(Arrays.asList(fromTable1, fromTable2))
.build();There is no async variant: get returns its GetResponse directly.
Sample response:
{
"records": [
{
"tableName": "table1",
"skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
"tokens": {
"card_number": [
{ "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" }
]
},
"data": { "card_number": "4111-XXXX-XXXX-1111", "cardholder_name": "John Doe" },
"hashedData": null,
"httpCode": 200,
"error": null,
"requestId": null
}
]
}GetResponseRecord carries the same fields as an insert record — the vault returns the same object shape for both — so the accessors are identical: getTableName(), getSkyflowId(), getTokens(), getData(), getHashedData(), getHttpCode(), getError(), getRequestId().
for (GetResponseRecord record : getResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getSkyflowId() + " -> " + record.getData());
} else {
System.err.println(record.getSkyflowId() + " failed ["
+ record.getHttpCode() + "] " + record.getError());
}
}Update records in a table by skyflow ID, in a single API call.
Vault type supported: structured (schema) vaults. See Vault type support.
Note:
tableNameis required on the request. A record may override it with its owntableName, which applies to that record only.- Every
UpdateRequestRecordneeds a non-blankskyflowId. dataholds the columns to change; no key or value may be blank.tokensis optional, and when supplied must be non-empty with no blank keys or values.updateTypeaccepts"UPDATE"(merge the supplied columns) or"REPLACE"(overwrite the whole record). Any other value fails validation; omitting it sends noupdateType, which the vault treats the same as"UPDATE". It is a request-level setting — records have noupdateTypeof their own.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.UpdateRequest;
import com.skyflow.vault.data.UpdateRequestRecord;
import com.skyflow.vault.data.UpdateResponse;
import com.skyflow.vault.data.UpdateResponseRecord;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class UpdateExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
// Step 1: Build the record — the columns to change, keyed by the record's skyflow ID
Map<String, Object> data = new HashMap<>();
data.put("cardholder_name", "jane doe");
UpdateRequestRecord record = UpdateRequestRecord.builder()
.skyflowId("9fac9201-7b8a-4446-93f8-5244e1213bd1")
.data(data)
.build();
// Step 2: Build the UpdateRequest
UpdateRequest updateRequest = UpdateRequest.builder()
.tableName("table1")
.records(Collections.singletonList(record))
.updateType("UPDATE")
.build();
// Step 3: Perform the update
UpdateResponse updateResponse = vault.update(updateRequest);
System.out.println(updateResponse);
}
}There is no async variant: update returns its UpdateResponse directly.
Sample response:
{
"records": [
{
"tableName": "table1",
"skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1",
"tokens": {
"cardholder_name": [
{ "token": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "tokenGroupName": "deterministic_string" }
]
},
"data": { "cardholder_name": "Jane Doe" },
"hashedData": null,
"httpCode": 200,
"error": null,
"requestId": null
}
]
}Like GetResponseRecord, UpdateResponseRecord carries the insert record's fields — the vault returns the same object shape — so the accessors are getTableName(), getSkyflowId(), getTokens(), getData(), getHashedData(), getHttpCode(), getError(), getRequestId().
for (UpdateResponseRecord record : updateResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getSkyflowId() + " updated");
} else {
System.err.println(record.getSkyflowId() + " failed ["
+ record.getHttpCode() + "] " + record.getError());
}
}Delete records from a table by skyflow ID or unique value, in a single API call.
Vault type supported: structured (schema) vaults. See Vault type support.
Note:
- This deletes the records themselves. Bulk Delete Tokens is a different operation — it removes tokens and leaves the underlying record in place.
tableNameis required, and exactly one ofskyflowIdsoruniqueValuesmust be supplied — both, or neither, fails validation.uniqueValuestakes the same shape as in Get: oneMap<String, Object>per record, holding the unique column-name/value pairs that identify it.
Continuing from Quickstart — skyflowClient below is the Skyflow client built there.
import com.skyflow.errors.SkyflowException;
import com.skyflow.vault.controller.VaultController;
import com.skyflow.vault.data.DeleteRequest;
import com.skyflow.vault.data.DeleteResponse;
import com.skyflow.vault.data.DeleteResponseRecord;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DeleteExample {
public static void main(String[] args) throws SkyflowException {
// skyflowClient is the Skyflow client built in Quickstart
VaultController vault = skyflowClient.vault();
List<String> ids = new ArrayList<>(Arrays.asList(
"9fac9201-7b8a-4446-93f8-5244e1213bd1",
"b2308e2a-c1f5-469b-97b7-1f193159399b"
));
DeleteRequest deleteRequest = DeleteRequest.builder()
.tableName("table1")
.skyflowIds(ids)
.build();
DeleteResponse deleteResponse = vault.delete(deleteRequest);
System.out.println(deleteResponse);
}
}There is no async variant: delete returns its DeleteResponse directly.
Sample response:
{
"records": [
{ "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "httpCode": 200, "error": null, "requestId": null },
{ "skyflowId": "b2308e2a-c1f5-469b-97b7-1f193159399b", "httpCode": 404, "error": "Record Not Found", "requestId": "a1b2c3d4-..." }
]
}DeleteResponseRecord is flatter than the insert-shaped records above — the vault returns no data, tokens, or hashed data for a delete. Accessors: deleteResponse.getRecords(), and on each record getSkyflowId(), getHttpCode(), getError(), getRequestId().
for (DeleteResponseRecord record : deleteResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getSkyflowId() + " deleted");
} else {
System.err.println(record.getSkyflowId() + " failed ["
+ record.getHttpCode() + "] " + record.getError());
}
}To include custom HTTP headers on an outgoing request — bulk or unary — pass a RequestInterceptor via that operation's options object. The headers available are defined by the CustomHeaderKey enum:
CustomHeaderKey |
HTTP header name |
|---|---|
SKYFLOW_ACCOUNT_ID |
x-skyflow-account-id |
SKYFLOW_ACCOUNT_NAME |
x-skyflow-account-name |
REQUEST_ID_HEADER |
x-request-id |
import com.skyflow.enums.CustomHeaderKey;
import com.skyflow.vault.data.BulkInsertOptions;
VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
BulkInsertOptions options = BulkInsertOptions.builder()
.interceptor(context -> context.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, "<YOUR_REQUEST_ID>"))
.build();
BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest, options);The interceptor runs once per batch, not once per bulk call — so a value generated inside it (a fresh request id, say) differs between the batches a single bulk call is split into. On a unary operation there is only ever one call, so it runs exactly once.
The same pattern applies to every operation, via its corresponding options class:
| Operation | Options class |
|---|---|
bulkInsert / bulkInsertAsync |
BulkInsertOptions |
bulkTokenize / bulkTokenizeAsync |
BulkTokenizeOptions |
bulkDetokenize / bulkDetokenizeAsync |
BulkDetokenizeOptions |
bulkDeleteTokens / bulkDeleteTokensAsync |
BulkDeleteTokensOptions |
insert |
InsertOptions |
detokenize |
DetokenizeOptions |
get |
GetOptions |
update |
UpdateOptions |
delete |
DeleteOptions |
This is the mental model to hold for every operation, bulk or unary:
| Layer | What it covers | How you see it |
|---|---|---|
| Request-level | The call could not be made or the whole call failed: invalid request shape, missing credentials, auth failure, payload over the 100,000-item limit. | A thrown SkyflowException. No results at all. |
| Record-level | The call succeeded, but individual records or tokens inside it did not. | A returned response. Nothing is thrown. Each entry in getRecords() reports its own httpCode and error. |
The second layer is what distinguishes flowvault from an all-or-nothing API: a call that returns normally can still contain failures, and a call where every single record failed also returns normally rather than throwing. Checking only for a thrown exception will silently miss failed records — always read the summary and the per-record results.
Unary operations follow the same two layers. Their records carry the same requestId behavior as bulk records — null on success, the failing call's x-request-id on error — the only structural difference is that unary records have no getIndex() (there is no batch position to report).
Every bulk response exposes getSummary() and getRecords(). The records list has one entry per submitted item, in the order you submitted it, and each entry carries:
| Field | Present on | Meaning |
|---|---|---|
getIndex() |
bulk only | Position of this item in the payload you submitted — use it to line results back up with your input. |
getHttpCode() |
always | Per-item status. 2xx for success; 4xx/5xx for failure. |
getError() |
failures only | Error message for this item. null means this item succeeded. |
getRequestId() |
failures only | The x-request-id of the call this item was part of — quote it in support escalations. In bulk responses, items from the same batch share one id. Present on both bulk and unary per-record types. |
The success payload sits alongside those fields on the same object: getSkyflowId()/getTokens()/getData() for insert (getFields() is deprecated — it returns the same data in its original, pre-typed Map<String, Object> shape, not getTokens()'s Token objects), getValue()/getTokenGroupName()/getMetadata() for detokenize, getValue()/getTokenGroupName()/getToken() for tokenize, getToken() for delete.
Summaries per operation:
| Response | Summary type | Fields |
|---|---|---|
BulkInsertResponse |
BulkSummary |
totalRecords, totalInserted, totalFailed |
BulkTokenizeResponse |
TokenizeSummary |
totalTokens, totalTokenized, totalPartial, totalFailed |
BulkDetokenizeResponse |
DetokenizeSummary |
totalTokens, totalDetokenized, totalFailed |
BulkDeleteTokensResponse |
DeleteTokensSummary |
totalTokens, totalDeleted, totalFailed |
A unary response has no summary and no getIndex() — just getRecords(), in submitted order, with getHttpCode(), getError(), and getRequestId() on each entry alongside that operation's payload: getSkyflowId()/getTokens()/getData()/getHashedData() for insert, get, and update; getSkyflowId() alone for delete; getToken()/getValue()/getTokenGroupName()/getMetadata() for detokenize.
The idiomatic way to consume a bulk response:
VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
BulkInsertResponse response = vault.bulkInsert(insertRequest);
System.out.println("inserted " + response.getSummary().getTotalInserted()
+ " of " + response.getSummary().getTotalRecords());
for (BulkInsertResponseRecord record : response.getRecords()) {
if (record.getError() == null) {
System.out.println("row " + record.getIndex() + " -> " + record.getSkyflowId());
} else {
System.err.println("row " + record.getIndex() + " failed ["
+ record.getHttpCode() + "] " + record.getError()
+ " (requestId " + record.getRequestId() + ")");
}
}Tokenize reports one entry per (value, token group) outcome, so a single value can partially succeed — several entries share its index:
for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) {
if (record.getError() == null) {
System.out.println(record.getIndex() + "/" + record.getTokenGroupName()
+ " -> " + record.getToken());
} else {
System.err.println(record.getIndex() + "/" + record.getTokenGroupName()
+ " failed [" + record.getHttpCode() + "] " + record.getError());
}
}SkyflowException covers the request-level layer only — client-side validation errors and whole-call API errors. It comes from common, so it is the same exception type skyvault throws.
import com.skyflow.errors.SkyflowException;
VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
try {
BulkInsertResponse response = vault.bulkInsert(insertRequest);
// reaching here means the CALL succeeded — individual records may still have failed
} catch (SkyflowException e) {
System.err.println("Skyflow error:");
System.err.println(" HTTP code : " + e.getHttpCode());
System.err.println(" Message : " + e.getMessage());
System.err.println(" Request ID: " + e.getRequestId());
System.err.println(" Details : " + e.getDetails());
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
}For the async variants, the same exception arrives wrapped in a CompletionException:
vault.bulkInsertAsync(insertRequest)
.thenAccept(response -> System.out.println(response))
.exceptionally(throwable -> {
System.err.println("bulk insert failed: " + throwable.getCause().getMessage());
return null;
});| Property | Method | Description |
|---|---|---|
| HTTP status code | getHttpCode() |
Integer status code (e.g. 400, 404, 500). |
| Message | getMessage() |
Human-readable description of the error. |
| HTTP status string | getHttpStatus() |
Status string from the server (e.g. "Bad Request" for a client-side validation error; for API errors, whatever string the server returns). |
| gRPC code | getGrpcCode() |
gRPC status code from the server. |
| Request ID | getRequestId() |
The x-request-id header — useful for support escalations. |
| Details | getDetails() |
JsonArray of additional error context from the server. Empty array for validation errors, null if the server response omitted the field. |
Validation errors (table name at the wrong level, empty token list, payload over 100,000 items, and similar) are thrown before any network call:
httpCodeis always400requestIdandgrpcCodearenulldetailsis an empty array
API errors are returned by the Skyflow server and have all fields populated from the response body and headers.
Because failures are reported per record, a partial failure can be retried without resubmitting the whole payload. Each response exposes a retry helper that filters its records down to the ones worth resending — server-side failures (HTTP 500–599), excluding 529, which is a permanent capacity-limit code:
| Response | Helper | Returns |
|---|---|---|
BulkInsertResponse |
getRecordsToRetry() |
List<BulkInsertRequestRecord> — your original record objects, ready to resubmit |
BulkTokenizeResponse |
getRecordsToRetry() |
List<BulkTokenizeRequestRecord> — values with at least one retryable token-group failure |
BulkDetokenizeResponse |
getTokensToRetry() |
List<String> — the tokens to resubmit |
BulkDeleteTokensResponse |
getTokensToRetry() |
List<String> — the tokens to resubmit |
VaultController vault = skyflowClient.vault(); // skyflowClient from Quickstart
BulkInsertResponse response = vault.bulkInsert(insertRequest);
List<BulkInsertRequestRecord> retryable = response.getRecordsToRetry();
if (!retryable.isEmpty()) {
BulkInsertResponse retryResponse = vault.bulkInsert(
BulkInsertRequest.builder()
.tableName("table1")
.upsert(UpsertOptions.builder().uniqueColumns(Arrays.asList("email")).build())
.records(new ArrayList<>(retryable))
.build());
}Client-side (4xx) failures are deliberately excluded — those need a fix to the data, not a retry. This is separate from the transport-level maxRetries setting in Timeouts and retries, which retries whole HTTP attempts and is off by default.