@stackshift-cloud/sdk
v1.2.0
Published
Official JavaScript and TypeScript SDK for StackShift.
Readme
@stackshift-cloud/sdk
Official JavaScript and TypeScript SDK for StackShift.
Governed media and galleries
Use a separate server-side client for each Assets space; the namespace is fixed on that client. Project membership does not grant Assets access.
const studio = new StackShift({
apiKey: process.env.STACKSHIFT_API_KEY,
assetSpaceId: 'your-asset-space-uuid',
})
const page = await studio.assets.dam.publications({ status: 'published' })
if (page.next_cursor) {
const next = await studio.assets.dam.publications({ status: 'published', cursor: page.next_cursor })
}
const gallery = await studio.assets.dam.resolveGallery('published-gallery-uuid')assets.dam also exposes collaborators, typed metadata schemas, bucket governance, publication transitions, model processing, gallery drafts and picker capabilities. Updates use explicit revisions. Submission needs assets:write, review needs assets:admin, and publication/withdrawal needs assets:publish, in addition to the corresponding Assets grant.
Keep account credentials on your server. Give @stackshift-cloud/assets-picker a short-lived, origin-restricted capability; give @stackshift-cloud/assets-gallery a server resolver for approved media. Store publication/gallery IDs and versions, never delivery URLs. Model processing is an explicitly enabled pilot and requires the qualified isolated worker described in packages/assets-model-processor/README.md.
Install
npm install @stackshift-cloud/sdkSet your API key:
STACKSHIFT_API_KEY=sk_live_xxxSend email
import { StackShift } from '@stackshift-cloud/sdk'
const stackshift = new StackShift()
await stackshift.mail.send({
from: 'StackShift <[email protected]>',
to: '[email protected]',
subject: 'Welcome',
text: 'Welcome to StackShift.',
})HTML and idempotency keys are supported:
const message = await stackshift.mail.send({
from: { email: '[email protected]', name: 'StackShift' },
to: ['[email protected]'],
subject: 'Verify your email',
html: '<p>Your code is 123456</p>',
text: 'Your code is 123456',
idempotencyKey: 'verify-email:user_123',
})
console.log(message.id, message.status, message.idempotencyStatus)Inspect MTA-accepted messages:
const messages = await stackshift.mail.messages.list({ status: 'mta_accepted', limit: 20 })
const detail = await stackshift.mail.messages.get(messages.data[0].id)
const attempts = await stackshift.mail.messages.attempts(detail.id)
const logs = await stackshift.mail.messages.logs(detail.id)mta_accepted means StackShift's outbound MTA accepted the message. delivered means the recipient MX accepted a recipient; it does not claim inbox placement or human receipt. Recipient records remain authoritative for mixed outcomes.
Send a template
Templates are created and rendered by the StackShift backend. Variables use {{name}} syntax, and missing variables fail before a message is queued.
await stackshift.mail.templates.create({
name: 'Welcome Email',
slug: 'welcome-email',
subject: 'Welcome, {{name}}',
html: '<h1>Welcome, {{name}}</h1>',
text: 'Welcome, {{name}}',
})
const preview = await stackshift.mail.templates.preview('welcome-email', {
data: { name: 'Ada' },
})
const message = await stackshift.mail.sendTemplate({
template: 'welcome-email',
to: '[email protected]',
from: 'Acme <[email protected]>',
data: { name: 'Ada' },
idempotencyKey: 'welcome:user_123',
})
console.log(preview.subject, message.id)Updating template content creates a new version. You can inspect or reactivate versions with templates.versions(), templates.getVersion(), and templates.activateVersion().
Bounces and suppressions
Hard bounces are automatically suppressed to protect sender reputation. Suppressions are workspace scoped and enforced by the StackShift backend before a message is queued.
const suppressions = await stackshift.mail.suppressions.list()
await stackshift.mail.suppressions.create({
email: '[email protected]',
reason: 'manual',
})
await stackshift.mail.suppressions.delete('sup_123')
const bounces = await stackshift.mail.bounces.list({
type: 'hard',
limit: 20,
})
const messageBounces = await stackshift.mail.messages.bounces('msg_123')bounced means StackShift detected a delivery failure after handoff. Hard-bounced recipients are automatically suppressed; removing a suppression allows future sends to be queued again.
Verify a sending domain
Custom domains are workspace/API-key scoped. You do not pass a project ID or deploy an app first.
const domain = await stackshift.mail.domains.create('acme.com')
console.log(domain.records)
await stackshift.mail.domains.verify(domain.id)
await stackshift.mail.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
subject: 'Welcome',
html: '<h1>Welcome</h1>',
text: 'Welcome',
})Add the returned SPF, DKIM, and return-path DNS records before verifying. DMARC is recommended by default and may be required if your StackShift environment sets MAIL_DMARC_REQUIRED=true. DNS propagation can take time, so a missing or pending status usually means you should wait and verify again.
Upload an asset
import { StackShift } from '@stackshift-cloud/sdk'
import { readFile } from 'node:fs/promises'
const stackshift = new StackShift()
const file = new Blob([await readFile('./avatar.png')], { type: 'image/png' })
const asset = await stackshift.assets.upload(file, {
folder: 'avatars',
visibility: 'public',
metadata: { userId: 'user_123' },
})
console.log(asset.url)You do not pass a deployed StackShift project ID. The API key identifies the StackShift account, and StackShift resolves the default asset space internally.
Private assets
const asset = await stackshift.assets.upload(file, {
bucket: 'imports',
key: 'customers.csv',
visibility: 'private',
})
const signed = await stackshift.assets.signedUrl(asset.id, {
expiresIn: '10m',
maxDownloads: 1,
})
console.log(signed.url)Image transformations
Use built-in presets or create your own named transformations.
import { StackShift, assetTransformOptions, getAssetTransformPreset } from '@stackshift-cloud/sdk'
const stackshift = new StackShift()
const hero = getAssetTransformPreset('hero')!
const heroOptions = assetTransformOptions(hero)
await stackshift.assets.createTransformation({ name: hero.name, ...heroOptions })
const named = stackshift.assets.namedUrl('asset_123', hero.name)
const signed = await stackshift.assets.signedTransformUrl('asset_123', {
...heroOptions,
expiresIn: '10m',
})
await stackshift.assets.deleteTransformation('old-preset')
console.log(named, signed.url)NestJS
import { Injectable } from '@nestjs/common'
import { StackShift } from '@stackshift-cloud/sdk'
@Injectable()
export class AssetsService {
private readonly stackshift = new StackShift({
apiKey: process.env.STACKSHIFT_API_KEY!,
})
upload(file: Express.Multer.File) {
return this.stackshift.assets.upload(
new Blob([file.buffer], { type: file.mimetype }),
{
bucket: 'uploads',
key: file.originalname,
visibility: 'private',
metadata: { originalName: file.originalname },
},
)
}
}Storage connections and migrations
Use AWS AssumeRole for S3 whenever possible. The returned external_id belongs in the role trust policy; credentials are write-only and are never returned by Stackshift.
const connection = await stackshift.assets.connections.create({
provider: 'aws_s3',
name: 'Production media',
configuration: {
bucket: 'acme-media',
region: 'us-east-1',
role_arn: 'arn:aws:iam::123456789012:role/StackshiftAssets',
auth_type: 'assume_role',
},
})
await stackshift.assets.connections.verify(connection.id)
const dryRun = await stackshift.assets.migrations.start({
source_connection_id: connection.id,
destination_bucket_id: 'assets_bucket_id',
source_prefix: 'legacy',
conflict_policy: 'skip',
dry_run: true,
})
const report = await stackshift.assets.migrations.downloadReport(dryRun.id, 'csv')Native Stackshift S2 storage uses a bucket reference and internal authorization, never copied S2 access keys:
const s2 = await stackshift.assets.storage.createS2Bucket({ name: 'asset-originals', region: 'us-east-1' })
await stackshift.assets.storage.configure('assets_bucket_id', 3, {
storage_mode: 'stackshift_s2',
s2_bucket_id: s2.id,
object_prefix: 'originals',
derivative_prefix: 'derived',
auto_extract_text: true,
})OCR and browser upload capabilities
const extraction = await stackshift.assets.text.start('asset_id', { language: 'eng' })
const current = await stackshift.assets.text.get('asset_id')
const capability = await stackshift.assets.uploadCapabilities.create({
bucket: 'uploads',
key_prefix: 'customers/user_123',
allowed_mime_types: ['image/png', 'image/jpeg'],
allowed_origins: ['https://app.example.com'],
max_bytes: 25 * 1024 * 1024,
expires_in: '15m',
})Create capabilities only in trusted backend code. Send capability.token to @stackshift-cloud/assets-widget; never put the Stackshift API key in a browser bundle.
For larger uploads, create a signed upload URL on your NestJS server and let the browser upload directly.
Direct browser uploads
Create a signed upload URL on your server:
const upload = await stackshift.assets.createUploadSession({
bucket: 'avatars',
key: 'users/user_123.png',
visibility: 'public',
expiresIn: '10m',
maxBytes: 5_000_000,
})Then upload from the browser:
await fetch(upload.url, {
method: upload.method,
body: file,
headers: { 'Content-Type': file.type },
})This is the recommended pattern for React, Next.js, TanStack Start, and any browser app. The browser never receives your StackShift API key.
StackShift chooses storage nodes, writes to StackShift-owned disks, replicates internally, and returns CDN-ready asset URLs. SDK users never choose storage nodes and never receive disk paths.
Campaign drafts and engagement
Create campaigns with content: { subject, html, text, editorMode: 'code' }, or keep using a template/version. mail.campaigns.update(id, { ...draft, revision }) saves a full draft; preview, test, send(id, revision), and schedule(id, sendAt, revision) support review and dispatch. A stale revision returns 409.
Mail send inputs accept tracking: { opens: false, clicks: true }. Each omitted setting inherits the workspace default. Use mail.tracking.settings() / updateSettings(), message(id) / campaign(id) for reports, and domains() / createDomain() / verifyDomain() / updateDomain() for branded hosts. Features must be enabled on the deployment. Opens are approximate; suspected automation is reported separately. Detailed activity expires after 90 days. Tests and OTPs never track.
Isolated testing, brands, migration, and spending
Use a sspat_test_ credential with simulation: { scenario: 'delayed' } for a 60-second simulated delay. Other outcomes are delivered, bounced, and complained. Test resources are isolated; no real email is delivered.
const brand = await client.mail.brands.create({ name: 'Product', editableFields: ['company'] })
const audience = await client.mail.audiences.create({ brandId: brand.id, name: 'Product news' })Every audience requires an explicit brand. Contact names/attributes are shared within that brand while consent stays per audience. mail.automations, mail.forms, and mail.segments expose audience-owned resources.
mail.migration supports public DNS audits/plans and authenticated checklists. mail.billing.summary() and mail.campaigns.estimate(id) expose allowance, credit, and blocking reasons. Pass approved maximum kobo as the third argument to send(id, revision, approvedMaxKobo) or the fourth to schedule(id, sendAt, revision, approvedMaxKobo). Zero is a valid hard maximum. Billing is inactive until a rate card is explicitly published and activated; funding and prepaid opt-in are separate owner-only dashboard actions.
Native video, governed DAM and product rendering
Use the space-scoped DAM and video clients for collaborators, metadata schemas, bucket governance, reviewed publications, picker capabilities, model preparation, product rendering, galleries, version-pinned encoding, captions and playback sessions.
const dam = sdk.assets.dam
const video = sdk.assets.video
const schema = await dam.createMetadataSchema({ name: 'Products', fields: [{
key: 'product_name', label: 'Product name', type: 'text', required: true,
}] })
const published = await dam.publishMetadataSchema(schema.id, schema.revision)
const workspace = await video.workspace(assetId)Mutations carry the supplied revision in If-Match. Model preparation and rendering carry an idempotency key; render submission also carries the accepted maximum units. Playback renewal and events authenticate with the session credential, not the account key. Keep these clients on your backend and return grants with Cache-Control: private, no-store.
See the Assets SDK guide and its linked feature guides for complete language examples, inputs, permissions, review transitions and browser callbacks.
