# TryDesk SDK for Node.js

Server-side client for TryDesk incoming messages and outgoing connector deliveries. Requires Node.js 22.12+ (22.x); no runtime dependencies. JavaScript (CommonJS/ESM) and TypeScript are supported. Never put the integration key in a browser or mobile application.

## Install

Install the public package after the first npm release:

```sh
npm install @trydesk/sdk@0.2.0
```

Package name: `@trydesk/sdk`. License: MIT (SDK only). Publishing the SDK does not grant access to the TryDesk service: create a workspace integration to obtain its server-side key.

### Local archive / offline development

From the TryDesk repository:

```sh
make sdk-pack
```

Copy `sdk/node/trydesk-sdk-0.2.0.tgz` into your connector's `functions/vendor/` directory, then install it from the functions directory:

```sh
npm install ./vendor/trydesk-sdk-0.2.0.tgz
```

Keep the archive in the connector repository so Firebase/CI can install the same version without access to the TryDesk checkout. Build SDK archives using Node 22.

## Text

```ts
import { TryDesk } from '@trydesk/sdk';

const desk = new TryDesk({ apiKey: process.env.TRYDESK_API_KEY! });
const result = await desk.messages.send({
  eventId: `telegram:${botId}:${update.update_id}`,
  externalConversationId: `telegram-chat-${chatId}`,
  sender: { externalId: String(userId), displayName: 'Alex', kind: 'customer' },
  text: 'Please help with my subscription',
});
```

The integration key determines workspace, source channel and reply route. No projectId or workspaceId is sent. For service alerts use `sender.kind: 'service'` and a stable sender ID such as `alarm`.

The default incoming endpoint is `https://api.trydesk.io/v1/messages`. While the custom domain's DNS or HTTPS certificate is pending, configure the working Hosting endpoint explicitly:

```ts
const desk = new TryDesk({
  apiKey: process.env.TRYDESK_API_KEY!,
  endpoint: 'https://helpdesk-1510b-api.web.app/v1/messages',
  deliveryEndpoint: 'https://helpdesk-1510b-api.web.app/v1/deliveries',
});
```

## Images

```ts
import { imageFromFile } from '@trydesk/sdk';

await desk.messages.send({
  eventId: 'source-event-123',
  externalConversationId: 'customer-42',
  sender: { externalId: '42' },
  text: 'Screenshot attached', // Optional for images
  images: [await imageFromFile('/tmp/screenshot.png')],
});
```

For downloaded files pass `{ data: Uint8Array | Blob, fileName, mimeType }`. Text uses JSON; images automatically use multipart/form-data, with one `payload` text field and repeated `images` files. Do not set Content-Type yourself.

Limits: 3 images, 10 MB each, 20 MB in total; PNG, JPEG, static WebP. The server checks image contents, a maximum of 16384 pixels per side and 40 megapixels. Text: 10000 characters; JSON: 64 KB. MB/KB here correspond to the byte limits exported in `LIMITS`.

## Retries and event IDs

The SDK defaults to 3 attempts with an independent 60-second timeout per attempt. It retries network failures, timeouts and HTTP 408/429/500/502/503/504. It respects Retry-After and never retries permanent validation/authentication errors. Redirects are not followed. Configure shorter limits for webhook handlers:

```ts
const desk = new TryDesk({
  apiKey: process.env.TRYDESK_API_KEY!,
  timeoutMs: 15000,
  maxAttempts: 2,
  maxRetryDelayMs: 2000,
});
```

One `send()` call snapshots metadata and image bytes and keeps the same event ID for all retries. If omitted, eventId is generated automatically. For Telegram use a deterministic ID from bot ID + update_id; for queued jobs persist the ID before the first attempt. A generated ID alone cannot prevent duplicates across process restarts or separate calls. Reusing the same ID with different content produces HTTP 409.

There is no persistent queue inside the SDK. If delivery still fails, retain the event and files in your job queue or return a failing webhook response so the source retries. An ambiguous network failure may mean that the message was already accepted: resend the same event unchanged.

```ts
import { TryDeskError } from '@trydesk/sdk';
try {
  await desk.messages.send(message, { signal: abortController.signal });
} catch (error) {
  if (error instanceof TryDeskError) {
    // Safe metadata; avoid logging keys, request bodies or original Telegram URLs.
    console.error({ code: error.code, status: error.status,
      eventId: error.eventId, retryable: error.retryable,
      attempts: error.attempts, retryAfterMs: error.retryAfterMs });
  }
  throw error;
}
```

Results include `eventId`, `conversationId`, `messageId`, `duplicate`. HTTP 200 means a previous event was already processed; 201 means newly accepted. `endpoint` may be overridden for self-hosted gateways; HTTPS is required except loopback HTTP for local tests. Use the same endpoint for retries.

## Development

```sh
make sdk-test
make incoming-api-test
```

The second command also checks the SDK against the real multipart/image parser without accessing Firebase. `docs/openapi/trydesk-incoming-v1.json` describes the public wire protocol.

## Outgoing deliveries (0.2.0)

Configure `deliveryEndpoint` (default `https://api.trydesk.io/v1/deliveries`). For a direct Cloud Functions endpoint use `https://europe-central2-helpdesk-1510b.cloudfunctions.net/deliveryApi`.

```ts
import { TryDesk, type DeliveryClaim } from '@trydesk/sdk';

const desk = new TryDesk({ apiKey: process.env.TRYDESK_API_KEY! });
type Message = NonNullable<DeliveryClaim['message']>;

// Call from a durable queue worker; sendOnce is your Telegram/email/etc. adapter.
async function deliver(deliveryId: string, sendOnce: (message: Message) => Promise<string[]>) {
  const task = await desk.deliveries.claim({ deliveryId });
  if (task.status !== 'ready') return;
  const lease = { deliveryId, leaseId: task.leaseId! };
  const started = await desk.deliveries.start(lease);
  if (started.status !== 'sending') return;

  let providerMessageIds: string[];
  try { providerMessageIds = await sendOnce(task.message!); }
  catch {
    // A provider timeout may mean it accepted the message. Do not resend blindly.
    await desk.deliveries.fail({ ...lease, outcome: 'unknown', code: 'provider_result_unknown' });
    return;
  }
  // Retry confirmation through the API; never call sendOnce again after it succeeded.
  await desk.deliveries.complete({ ...lease, providerMessageIds });
}
```

`claim` generates one requestId per call and preserves it during HTTP retries. An active lease blocks competing workers. `complete` is idempotent for the same IDs. A lease started but interrupted becomes `unknown`, not automatically resendable. Only the current assigned operator can explicitly authorize a retry of an unknown result. Late confirmation with the same lease can still mark it sent.

Use `deliveries.fail({ ...lease, outcome: 'retry' | 'failed' | 'unknown', code, retryAfterMs?, providerMessageIds? })`. Retry only after a definite rejection (e.g. Telegram 429). After a timeout or partial multi-part send, use unknown; never blindly repeat the provider call. No lease API can guarantee exactly-once side effects at a provider without an idempotency mechanism.

The key scopes requests to one active workspace/integration with replies enabled. Delivery data is universal: text and recipient.externalConversationId/externalCustomerId. Telegram interpretation belongs to the connector. This SDK version implements text deliveries only.

`skip` with `reason: 'waiting_for_previous'` means the predecessor will wake this task when it completes. Acknowledge this notification. `sent` is acceptance by the external provider, not a read receipt. Do not log the key, text, lease or token-bearing provider URLs.

## Documentation

- [Integration guide and API downloads](https://docs.trydesk.io/en/)
- [Incoming OpenAPI](https://docs.trydesk.io/incoming.openapi.json)
- [Delivery OpenAPI](https://docs.trydesk.io/delivery.openapi.json)
- [Postman collection](https://docs.trydesk.io/trydesk.postman_collection.json)

English is the default language. [Русская версия](https://docs.trydesk.io/ru/). The previous API-hosted documentation URLs remain available for compatibility.
