Skip to content

hailer.credential ​

Store secrets your app needs — an API key for a third-party service, a username/password pair, or any custom JSON payload — encrypted at rest in Hailer's credential vault, and read them back when you need them. Use it together with http so an outgoing request's token never has to live in your app bundle or its config.

MethodHost opReturns
create(name, type, secret)credential.createPromise<{ _id: string }>
list()credential.listPromise<CredentialListItem[]>
get(credentialId)credential.getPromise<CredentialDetails>
update(credentialId, updates)credential.updatePromise<boolean>

Each host op scopes the call to your app and then proxies it to the backend's v3.credential.* vault operation.

Scoping — what your app can reach ​

The host binds every credential you create to your app's id (appId) and re-checks that binding on every read and write, so one app can never reach another app's credentials. On top of that:

  • create always makes a personal credential bound to your app. Workspace scope and readers are dropped by the host — the SDK cannot create workspace-shared credentials.
  • list and get return your app's credentials the acting user may read. For a workspace app that also includes workspace credentials tagged with your app — only a workspace admin or owner can create those, from workspace settings. The backend enforces the credential's readers, with one exception: workspace admins and owners read and list every workspace credential tagged with your app, whether or not they appear in readers. Don't treat readers as a guarantee that an admin using your app can't see a credential.
  • update works on personal credentials only. Updating a workspace credential through the SDK is rejected — those are managed in Hailer's workspace settings.
  • There is no remove. Credentials are deleted by their owner in Hailer, not by apps.

Since the vault is per-user, each user of your app stores their own credentials: what list returns for one user says nothing about another.

Credential types ​

typeSecret shape
'basic'{ username: string; password: string }
'apiKey'{ apiKey: string }
'custom'Any JSON object, 1–50 top-level keys

The type is a label for how your app should read the payload — the vault encrypts whatever object you pass either way. Keep it honest so the credential renders correctly in Hailer's credential UI.

create(name, type, secret) ​

ts
const { _id } = await hailer.credential.create('Acme API', 'apiKey', { apiKey: 'sk-…' });

await hailer.credential.create('SFTP login', 'basic', {
  username: 'reports',
  password: 'hunter2',
});

await hailer.credential.create('Webhook config', 'custom', {
  url: 'https://example.com/hook',
  token: 'abc123',
});

name is 1–128 characters. Resolves with the new credential's id — store that id (in your app config, for example) so you can get the secret later.

list() ​

ts
const credentials = await hailer.credential.list();
// [{ _id, name, type, appId, workspaceId?, readers?, createdBy, created, updated }]

Metadata only — the encrypted payload is never included, so this is the call to use for "which credential should I use?" pickers. workspaceId is present only on workspace credentials; personal ones omit it. Takes no arguments: any filter would be ignored, as the host always scopes results to your app.

get(credentialId) ​

ts
const { type, secret } = await hailer.credential.get(credentialId);

const response = await hailer.http.fetch('https://api.acme.com/v1/orders', {
  headers: { authorization: `Bearer ${secret.apiKey}` },
});

Decrypts and returns the payload directly — there is no token exchange step. Optionally pass the expected secret shape:

ts
import { CredentialBasicData } from '@hailer/app-sdk';

const { secret } = await hailer.credential.get<CredentialBasicData>(credentialId);
secret.username; // typed

Rejects if the credential doesn't exist, isn't bound to your app, or the user may not read it. Treat the result as sensitive: keep it in memory for the call that needs it, and don't log it or write it into activity fields.

update(credentialId, updates) ​

ts
// Rename
await hailer.credential.update(credentialId, { name: 'Acme API (production)' });

// Rotate the secret
await hailer.credential.update(credentialId, { secret: { apiKey: 'sk-new…' } });

At least one of name or secret must be provided. secret replaces the whole stored payload — there is no key-level merge, so a partial object drops the keys you leave out. To change one key of a custom credential, get it, spread it, and send the full object back. Resolves true on success.

Errors ​

Every method throws HailerError. The cases worth handling:

SituationWhat you see
Credential not bound to your app, or missingCredential not found or not bound to this app (403)
Updating a workspace credentialCredential not found or not editable by this app (403)
Invalid name, type, or empty secretValidation error from the backend

Related: http for using a credential in an outgoing request, and Private vs workspace apps for what the two app scopes change.

Hailer Developer Documentation · v1.8.0