Appearance
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.
| Method | Host op | Returns |
|---|---|---|
create(name, type, secret) | credential.create | Promise<{ _id: string }> |
list() | credential.list | Promise<CredentialListItem[]> |
get(credentialId) | credential.get | Promise<CredentialDetails> |
update(credentialId, updates) | credential.update | Promise<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:
createalways makes a personal credential bound to your app. Workspace scope andreadersare dropped by the host — the SDK cannot create workspace-shared credentials.listandgetreturn 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'sreaders, with one exception: workspace admins and owners read and list every workspace credential tagged with your app, whether or not they appear inreaders. Don't treatreadersas a guarantee that an admin using your app can't see a credential.updateworks 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
type | Secret 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; // typedRejects 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:
| Situation | What you see |
|---|---|
| Credential not bound to your app, or missing | Credential not found or not bound to this app (403) |
| Updating a workspace credential | Credential not found or not editable by this app (403) |
Invalid name, type, or empty secret | Validation 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.