TypeScript SDK for the qURL™ API — secure, time-limited access links for AI agents.
Quantum URL (qURL) · The internet has a hidden layer. This is how you enter.
AI agents need to access protected resources — APIs, databases, internal tools — but giving them permanent credentials is a security risk. qURL creates time-limited, auditable access links that expire automatically. The SDK handles authentication, retries, pagination, and error handling so you can focus on your agent logic.
npm install @layervai/qurlRequires Node.js 18+. Both import { QURLClient } from '@layervai/qurl' (ESM) and const { QURLClient } = require('@layervai/qurl') (CJS) work.
import { QURLClient } from '@layervai/qurl';
const client = new QURLClient({ apiKey: 'lv_live_xxx' });
// Create a protected link
const result = await client.create({
target_url: 'https://api.example.com/data',
expires_in: '24h',
label: 'API access for agent',
});
console.log(result.qurl_link);
// Resolve a token (grants network access for your IP)
const access = await client.resolve('at_...');
console.log(`Access granted to ${access.target_url} for ${access.access_grant?.expires_in}s`);| Option | Required | Default |
|---|---|---|
apiKey |
Yes | — |
baseUrl |
No | https://api.layerv.ai |
maxRetries |
No | 3 |
timeout |
No | 30000 (ms) — per attempt, not total |
fetch |
No | globalThis.fetch |
userAgent |
No | qurl-typescript/<version> |
debug |
No | false |
| Method | Description |
|---|---|
create(input) |
Create a protected link |
batchCreate(input) |
Create up to 100 protected links in one request |
get(id) |
Get qURL details |
list(input?) |
List qURLs (single page) |
listAll(input?) |
Iterate all qURLs (auto-paginating) |
delete(id) |
Revoke a qURL resource and all its tokens |
extend(id, input) |
Extend expiration |
update(id, input) |
Update qURL resource properties |
mintLink(id, input?) |
Mint a new access link |
resolve(input) |
Resolve token + grant network access |
getQuota() |
Get quota/usage info |
bootstrapAgent(input) |
Bootstrap a qURL Connector agent |
listResources(input?) / listAllResources(input?) / createResource(input) / getResource(id) |
Resource management |
updateResource(id, input) / deleteResource(id) |
Update or revoke resources |
createQurlForResource(id, input?) |
Mint a qURL for an existing resource |
updateResourceQurl(id, qurlId, input) / revokeResourceQurl(id, qurlId) |
Manage one token on a resource |
listResourceSessions(id) / terminateAllResourceSessions(id) / terminateResourceSession(id, sessionId) |
Inspect or terminate active sessions |
listConnectorInstallations(input?) / listAllConnectorInstallations(input?) |
List connector installations |
getUsageCurrentPeriod() / getUsageDaily() |
Usage reporting |
getCustomer() / updateCustomer(input) |
Customer settings |
createBillingCheckout(input) / createBillingPortal() / listBillingInvoices(input?) / listAllBillingInvoices(input?) |
Billing flows |
registerDomain(input) / listDomains(input?) / listAllDomains(input?) / getDomain(domain) |
Custom domain management |
verifyDomain(domain) / regenerateDomainToken(domain) / deleteDomain(domain) |
Domain verification and removal |
listWebhooks(input?) / listAllWebhooks(input?) / createWebhook(input) / getWebhook(id) |
Webhook management |
updateWebhook(id, input) / deleteWebhook(id) / regenerateWebhookSecret(id) |
Webhook updates and secret rotation |
listWebhookEventTypes() / listWebhookDeliveries(id, input?) / listAllWebhookDeliveries(id, input?) |
Webhook metadata and delivery history |
createApiKey(input) / listApiKeys(input?) / listAllApiKeys(input?) / updateApiKey(id, input) / revokeApiKey(id) |
API key management |
createAccessCode(input) / listAccessCodes() / redeemAccessCode(input) / revokeAccessCode(id) |
Access code management |
listResourceSessions(id) and listAccessCodes() reflect currently unpaginated service endpoints. Their outputs always return has_more: false; if the service starts surfacing cursor metadata, the SDK emits a debug log rather than exposing an unactionable next-page signal.
listAll*() methods validate ids and query params when called, before the async iterator is consumed. Wrap the listAll*() call itself in try/catch when passing dynamic input.
Create up to 100 qURLs in a single request. Does not throw on partial or total failure — per-item errors are returned in the results array, so try/catch alone won't surface them. Always inspect result.failed and iterate result.results:
const result = await client.batchCreate({
items: [
{ target_url: 'https://api.example.com/data', expires_in: '24h' },
{ target_url: 'https://api.example.com/admin', expires_in: '1h' },
],
});
if (result.failed > 0) {
for (const r of result.results) {
if (!r.success) {
console.error(`items[${r.index}]: ${r.error.code} - ${r.error.message}`);
}
}
}Non-400 errors (401, 403, 429, 5xx, and unexpected 400 body shapes) still throw the appropriate QURLError subclass.
Slimmer per-item shape — BatchItemSuccess returns { resource_id, qurl_link, qurl_site, expires_at? } per item. Unlike single client.create(), the batch response intentionally omits qurl_id and label to keep the payload compact. If you migrate a per-item create() loop to batchCreate and rely on qurl_id for downstream addressing, fetch each via client.get(resource_id) after the batch (or stay on the single-create path).
Result ordering — result.results is not guaranteed to be sorted by index. Each entry's index field carries the position in the original items array, so build per-input-position state by keying on r.index (e.g., for (const r of result.results) { byInputIndex[r.index] = r; }) rather than relying on iteration order.
Out-of-range or duplicate index values — the SDK throws QURLError (code: "unexpected_response") on either condition, since both indicate server misbehavior that would silently break per-item attribution (a Map keyed on r.index would last-write-wins, an out-of-range index would attribute to a non-existent slot).
All API errors throw typed error subclasses, so you can catch specific failure modes:
import {
QURLError,
AuthenticationError,
NotFoundError,
RateLimitError,
ValidationError,
} from '@layervai/qurl';
try {
await client.create({ target_url: '' });
} catch (err) {
if (err instanceof ValidationError) {
console.error('Invalid input:', err.invalidFields);
} else if (err instanceof RateLimitError) {
console.error(`Rate limited — retry after ${err.retryAfter}s`);
} else if (err instanceof AuthenticationError) {
console.error('Bad API key');
} else if (err instanceof NotFoundError) {
console.error('Resource not found');
} else if (err instanceof QURLError) {
console.error(`API error [${err.code}]: ${err.detail}`);
}
}| Error Class | HTTP Status | When |
|---|---|---|
AuthenticationError |
401 | Invalid or missing API key |
AuthorizationError |
403 | Key lacks required scope |
NotFoundError |
404 | Resource doesn't exist |
ValidationError |
400, 422 | Invalid request body |
RateLimitError |
429 | Too many requests |
ServerError |
5xx | Server-side failure |
NetworkError |
— | Connection failure |
TimeoutError |
— | Request exceeded timeout |
// Single page
const page = await client.list({ limit: 10, status: 'active' });
// Auto-paginate through all results
for await (const qurl of client.listAll({ status: 'active' })) {
console.log(qurl.resource_id);
}Enable debug output to see all HTTP requests and retries:
// Log to console
const client = new QURLClient({ apiKey: 'lv_live_xxx', debug: true });
// Custom logger
const client = new QURLClient({
apiKey: 'lv_live_xxx',
debug: (message, data) => myLogger.debug(message, data),
});The client automatically retries failed requests with exponential backoff:
- GET/DELETE: Retries on 429, 502, 503, 504
- POST/PATCH: Retries status responses only on 429
- Network errors: Always retried; POST/PATCH requests send an
Idempotency-Keyon the first attempt and reuse it on retries Retry-Afterheader: Honored on 429 and 503 responses (RFC 7231 §7.1.3). Currently the SDK only parses delta-seconds values (e.g.Retry-After: 30); HTTP-date values (Retry-After: Wed, 21 Oct 2026 07:28:00 GMT) silently fall back to exponential backoff. Tracked in #61.
Configure with maxRetries (default: 3). Set to 0 to disable.
Worst-case latency:
timeoutis enforced per attempt, not for the whole request. Total worst-case latency is roughlytimeout × (maxRetries + 1) + sum(retry delays). Operators tuningtimeoutshould account for this when sizing health-check budgets.
For POST/PATCH requests, the SDK generates a UUIDv7 Idempotency-Key once per logical call and reuses it across SDK-managed retries, so the API can return the original result instead of creating duplicate resources. If your application catches an error and calls the SDK again, pass a stable override so the new call deduplicates with the first one. Caller-provided keys must be non-empty printable ASCII strings of at most 256 characters and must not start or end with spaces. Use a unique key for each logical operation; reusing one key for a different request can return the first cached response. To tie retries to your own upstream job or request ID, pass a per-call override:
await client.create(
{ target_url: 'https://api.example.com/data' },
{ idempotencyKey: 'job_12345_create_qurl' },
);SDK-generated keys require globalThis.crypto.getRandomValues, which is available in supported Node 20+ runtimes and modern edge/browser runtimes. In constrained runtimes without Web Crypto, pass a caller-provided key with idempotencyKey; otherwise POST/PATCH calls throw RuntimeError before sending a request.
This SDK is pre-1.0; breaking changes between minor versions are possible until the API surface stabilizes. Significant changes are called out in CHANGELOG.md and in the corresponding GitHub release notes.
When upgrading, check the release notes for migration guidance — recent breaking changes have included field renames (description → label on create), removed fields (metadata), narrowed type unions (QURL.status), and endpoint relocations (/v1/qurl → /v1/qurls).
MIT