@sume-com/client
v0.3.1
Published
Official Sume SDK for Node.js and TypeScript
Readme
Sume SDK for Node.js and TypeScript
Official SDK for Sume's image and video generation API, the Sume Open API at https://api.open.sume.com.
Installation
npm install @sume-com/clientClient
The client is exported from @sume-com/client/v2 and from the package root. It features:
- Single
credentialsfield: one Sume API key (sume_live_...) Authorization: Bearer <key>authentication headerUser-Agent: sume-server-js/<version>header- Server-side only (browser usage blocked for security)
- Simplified API with
subscribe()method: name a model, or a router path - Automatic polling via
/v1/jobs/{id}/status, then the result from/v1/jobs/{id}/result - Safe retries: every submit carries an
Idempotency-Key, so a retried submit never makes a second, billed job
Quick Start
import { sume, config } from '@sume-com/client/v2';
// Configure credentials
config({
credentials: 'YOUR_SUME_API_KEY'
});
// Generate content
const response = await sume.subscribe('gpt-image-2', {
input: {
prompt: 'A ceramic coffee cup on a wooden table, soft morning light',
quality: 'low',
aspect_ratio: '1:1'
},
withPolling: true
});
if (response.status === 'completed') {
console.log('Image URL:', response.images?.[0].url);
}gpt-image-2 at quality: 'low' costs $0.006 an image. Every model and its dollar price is in the open model catalog.
Authentication
Make a key at open.sume.com → API keys. A key spends your open.sume.com balance, in US dollars. Keep it on your server.
Option 1: Single credentials field (recommended)
import { config } from '@sume-com/client/v2';
config({
credentials: 'YOUR_SUME_API_KEY'
});Option 2: apiKey field
config({
apiKey: 'YOUR_SUME_API_KEY'
});Option 3: Environment variable
export SUME_API_KEY="sume_live_..."// No config needed - automatically reads SUME_API_KEY from the environment
import { sume } from '@sume-com/client/v2';Creating a Client Instance
import { createSumeClient } from '@sume-com/client/v2';
const client = createSumeClient({
credentials: 'YOUR_SUME_API_KEY'
});Explicit client instances keep their credentials separate. client.configure() updates only that instance; module-level config() updates the shared sume client.
API Methods
subscribe(endpoint, options)
Submit a job and wait for its result:
const response = await sume.subscribe('seedance-2.5', {
input: {
prompt: 'Steam rises from the cup as the camera slowly pushes in',
duration: 5,
resolution: '480p',
aspect_ratio: '16:9'
},
withPolling: true, // Automatically poll for job completion (default: true)
webhook: { // Optional webhook
url: 'https://your-webhook-url.com/callback'
}
});Parameters:
endpoint(string): A model id from the open model catalog (e.g.,'gpt-image-2'), or an API path withmodelin the input:'/v1/image-router/generate'for images,'/v1/video-router/generate'for video, lip-sync and captionsoptions.input(object): Input parameters for the model (prompt,aspect_ratio,duration,image_urland so on)options.withPolling(boolean, default:true): Automatically poll for job completionoptions.webhook(object, optional): Webhook configurationurl(string): Public HTTPS URL that Sume calls when the job ends
options.idempotencyKey(string, optional): Your ownIdempotency-Key(1 to 255 printable ASCII characters). Without it, the SDK makes a UUID for each call
Note: When a webhook is provided, the SDK sends mode: "webhook" and webhook_url in the request body.
Sume runs every model behind two routers. For a model id, the SDK reads the model's kind once from the public catalog (GET /v1/open/catalog/{id}), then submits to the image router (image) or the video router (video, lipsync, captions) with model in the body. An unknown id fails there, before any job. Each model's fields and accepted values are in its catalog row: image models, video models.
upload(data, contentType)
Upload a file that is not online yet, and get an HTTPS URL to pass to a model:
import { readFile } from 'node:fs/promises';
const audioUrl = await sume.upload(await readFile('speech.wav'), 'audio/wav');The SDK asks POST /v1/files/generate-upload-url for a place to put the file, PUTs the bytes there without your API key, and returns the file's public_url. Uploading is free.
uploadImage(imageBuffer, format?)
upload() for an image: format is 'jpeg' (default), 'png' or 'webp'.
const imageUrl = await sume.uploadImage(await readFile('photo.png'), 'png');Examples
Text-to-Image Generation
import { sume, config } from '@sume-com/client/v2';
config({
credentials: 'YOUR_SUME_API_KEY'
});
const response = await sume.subscribe('nano-banana-pro', {
input: {
prompt: 'A majestic mountain landscape at sunset',
aspect_ratio: '16:9'
},
withPolling: true
});
if (response.status === 'completed') {
console.log('Image URL:', response.images?.[0].url);
}Image Editing
const response = await sume.subscribe('gpt-image-2', {
input: {
prompt: 'Make the cup cobalt blue, keep everything else the same',
quality: 'low',
image_urls: ['https://example.com/cup.png']
}
});Image-to-Video Generation
import { readFile } from 'node:fs/promises';
const imageUrl = await sume.uploadImage(await readFile('cup.png'), 'png');
const response = await sume.subscribe('/v1/video-router/generate', {
input: {
model: 'seedance-2.5',
prompt: 'Cinematic camera movement around the cup',
image_url: imageUrl,
duration: 5,
resolution: '480p'
},
withPolling: true
});
if (response.status === 'completed') {
console.log('Video URL:', response.video?.url);
}Lip-sync
const response = await sume.subscribe('veed-fabric-1.0', {
input: {
image_url: await sume.uploadImage(await readFile('face.jpg')),
audio_url: await sume.upload(await readFile('speech.wav'), 'audio/wav'),
duration: 8,
resolution: '480p'
}
});Polling & Status Lifecycle
The client automatically polls /v1/jobs/{id}/status when withPolling is true. It waits as long as the API asks (next_poll_after_seconds, 2 seconds while a job runs) and never less than pollInterval. It keeps polling through 5xx, 429 and dropped connections. When the job completes, it reads /v1/jobs/{id}/result.
Status Values:
queued– Request accepted and waiting for a workerin_progress– Generation currently runningcompleted– Generation finished and media URLs are returnednsfw– Content refused by the model's or Sume's content policy (error.public_reasoniscontent_policy_rejected); the price is refundedfailed– Generation errored; the price is refunded anderrorsays whycanceled– Canceled before generation started; the price is refunded
API Response Format:
The status call answers with Sume's job status:
{
"data": {
"request_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
"job_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
"status": "COMPLETED",
"sume_status": "completed",
"status_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/status",
"result_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/result",
"cancel_url": null,
"next_action": "fetch_result",
"terminal": true
}
}The SDK turns it, and the result's artifacts, into one V2Response:
{
"status": "completed",
"request_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
"status_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/status",
"cancel_url": null,
"result_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/result",
"images": [{ "url": "https://media.sume.com/artifacts/artf_.../images.png" }],
"artifacts": [
{
"id": "artf_...",
"type": "image",
"url": "https://media.sume.com/artifacts/artf_.../images.png",
"content_type": "image/png"
}
]
}images holds every image artifact and video the first video artifact. artifacts keeps every artifact as Sume returns it, audio included. A failed job resolves with status: 'failed' (or 'nsfw') and error (code, message, public_reason), not an exception. Artifacts live on media.sume.com and are readable by anyone with the link; copy them to your own storage if you need to control access.
Working with the Response
const response = await sume.subscribe('gpt-image-2', {
input: { prompt: 'Test', quality: 'low', num_images: 2 },
withPolling: true
});
// Check status
console.log('Request ID:', response.request_id);
console.log('Status:', response.status);
// Access results
for (const image of response.images ?? []) {
console.log('Result URL:', image.url);
}
if (response.status === 'failed' || response.status === 'nsfw') {
console.error(response.error?.code, response.error?.message);
}Long Jobs and Resuming
maxPollTime (5 minutes by default) bounds the wait. When it runs out, subscribe() throws TimeoutError, and the job keeps running. Do not submit a new job: call subscribe() again with the same input and the error's idempotencyKey. Sume returns the same job, charges it once, and the SDK keeps waiting.
import { TimeoutError } from '@sume-com/client/v2';
const input = { prompt: 'A paper boat drifting down a rain gutter', duration: 10 };
try {
await sume.subscribe('seedance-2.5', { input });
} catch (error) {
if (error instanceof TimeoutError) {
const response = await sume.subscribe('seedance-2.5', {
input,
idempotencyKey: error.idempotencyKey
});
console.log(response.video?.url);
}
}The same works across processes: pass your own idempotencyKey, store it, and submit with withPolling: false. Later, the same call with withPolling: true waits for that job.
Webhooks
await sume.subscribe('kling-3', {
input: { prompt: 'A slow dolly shot through a neon market at night', duration: 5 },
webhook: { url: 'https://example.com/hooks/sume' },
withPolling: false
});Sume POSTs job.completed, job.failed or job.canceled to the URL once the job ends, and retries for about three hours until it gets a 2xx. Every delivery is signed with your workspace's signing secret (dashboard → Webhooks). Check it with verifyWebhook on the raw body before you trust the event:
import { verifyWebhook, WebhookEvent } from '@sume-com/client';
export async function POST(request: Request) {
const body = await request.text();
const valid = verifyWebhook(
body,
request.headers.get('x-sume-webhook-timestamp') ?? undefined,
request.headers.get('x-sume-webhook-signature') ?? undefined,
process.env.SUME_WEBHOOK_SECRET!
);
if (!valid) {
return new Response('invalid signature', { status: 401 });
}
const event = JSON.parse(body) as WebhookEvent;
console.log(event.event, event.job_id, event.payload?.artifacts[0]?.url);
return new Response('ok');
}verifyWebhook accepts any sume-v1= signature in the header (there is one per live secret during a rotation) and refuses deliveries more than five minutes old. A delivery can arrive more than once: dedupe on job_id.
Configuration
import { createSumeClient } from '@sume-com/client/v2';
const client = createSumeClient({
credentials: 'YOUR_SUME_API_KEY',
// Optional configuration
baseURL: 'https://api.open.sume.com', // Default
timeout: 120000, // 2 minutes default
maxRetries: 3,
retryBackoff: 1000,
retryMaxBackoff: 60000,
pollInterval: 2000, // Check every 2 seconds at most
maxPollTime: 300000, // Timeout after 5 minutes
headers: {
'X-Custom-Header': 'value'
}
});Retries cover network errors, 429 and 5xx, with exponential backoff and jitter, and follow retry-after. A 4xx is never retried. Raise maxPollTime for long video, which can take several minutes.
TypeScript Support
import {
createSumeClient,
SumeClient,
V2Response
} from '@sume-com/client/v2';
const client: SumeClient = createSumeClient({
credentials: 'YOUR_SUME_API_KEY'
});
const response: V2Response = await client.subscribe('/v1/image-router/generate', {
input: {
model: 'gpt-image-2',
prompt: 'Test',
aspect_ratio: '1:1'
}
});The two router paths type their input (ImageGenerateInput, VideoGenerateInput): model is required, the documented fields are checked, and model-specific fields pass through. A model id takes any input object.
Error Handling
The SDK provides comprehensive error handling with specific error types:
import {
AuthenticationError,
BadInputError,
ValidationError,
NotEnoughCreditsError,
APIError,
TimeoutError,
BrowserNotSupportedError
} from '@sume-com/client';
try {
const response = await sume.subscribe('gpt-image-2', {
input: { prompt: 'Test' }
});
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('❌ Authentication failed - check your API key');
} else if (error instanceof NotEnoughCreditsError) {
console.error('💳 Insufficient funds - add funds at open.sume.com/billing');
} else if (error instanceof BadInputError) {
console.error('📋 Invalid input parameters:', error.message, error.details);
} else if (error instanceof ValidationError) {
console.error('⚠️ Validation error:', error.message);
} else if (error instanceof TimeoutError) {
console.error('⏱️ Still running, resume with idempotencyKey', error.idempotencyKey);
} else if (error instanceof APIError) {
console.error('🌐 API Error:', error.statusCode, error.code, error.requestId, error.message);
} else if (error instanceof BrowserNotSupportedError) {
console.error('🚫 Browser usage not supported - use Node.js environment');
} else {
console.error('💥 Unexpected error:', error);
}
}| Class | When | Sume code |
|---|---|---|
| AuthenticationError | 401 | unauthorized |
| NotEnoughCreditsError | 402 | insufficient_funds; responseData.error.details has required_usd and available_usd |
| BadInputError | 400 | invalid_request (details lists each field and why), concurrency_limit_reached, bad_request |
| ValidationError | 422 | |
| APIError | any other status | open_key_route_not_allowed (403), not_found and model_not_found (404), idempotency_conflict (409), rate_limited (429), database_busy (503), internal_error (500) |
| TimeoutError | maxPollTime ran out; the job keeps running | |
| CredentialsMissedError | no key in config or SUME_API_KEY | |
| BrowserNotSupportedError | the SDK was loaded in a browser | |
Every API error carries statusCode, Sume's stable code, the requestId to quote to support, and the full body in responseData. A network error is an APIError with no statusCode and the system code (ECONNRESET, ENOTFOUND, ...). Errors never carry your API key or a signed upload URL. All of them extend SumeError.
A workspace may have only so many jobs queued or running at once (2 to 40, by the funds added in the past 28 days). A submit past the cap is refused with BadInputError and code: 'concurrency_limit_reached', and is not charged. Wait for a job to end, then submit the next.
Job Status Handling
switch (response.status) {
case 'completed':
console.log('Success:', response.images?.[0]?.url ?? response.video?.url);
break;
case 'failed':
console.error('Generation failed:', response.error?.message);
break;
case 'nsfw':
console.warn('Content flagged as NSFW');
break;
case 'canceled':
console.warn('Job was canceled');
break;
}Best Practices
Name the model yourself: prices differ by more than 100× between models. The catalog lists every model's dollar price, and Estimate a price prices a request for free.
Upload large files: a local image, clip or audio file goes up once with
upload()/uploadImage(), and its URL works in any input field:const imageUrl = await sume.uploadImage(localImageBuffer, 'jpeg');Handle rate limits: configure exponential backoff for retries:
const client = createSumeClient({ credentials: 'YOUR_SUME_API_KEY', maxRetries: 5, retryBackoff: 2000, retryMaxBackoff: 30000 });Use webhooks for long operations: for video, a webhook saves the polling. Keep a slow poll as a fallback in case a delivery is lost.
Never resubmit a running job: after a
TimeoutErroror a crash, callsubscribe()again with the sameidempotencyKey. A new key is a new, billed job.Environment variables: store your key in the environment:
export SUME_API_KEY="sume_live_..."
Support
- Documentation: https://open.sume.com/docs
- API Status: https://status.sume.com
License
MIT
