Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@textbee/sdk",
"version": "0.3.1",
"version": "0.4.0",
"description": "Official JavaScript SDK for textbee.dev, the open source SMS gateway",
"license": "MIT",
"packageManager": "pnpm@9.14.2",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type {
ListMessagesOptions,
Message,
MessageDevice,
MessageIssueEntry,
MessageIssueSource,
MessageList,
MessageListMeta,
SendSmsImmediateResponse,
Expand Down
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ export interface MessageDevice {
enabled?: boolean
}

/** Where a failure was reported from. */
export type MessageIssueSource = 'push' | 'device' | 'server'

/** One failure a message had. Kept after the message later succeeds. */
export interface MessageIssueEntry {
code: string
message?: string
/** When the failure happened. ISO date. */
at: string
source: MessageIssueSource
/** Set on entries rebuilt from old message fields. */
backfilled?: boolean
/** True when the entry is a delivery report that did not confirm delivery. The message was sent; this is not a send failure. */
deliveryReport?: boolean
}

export interface Message {
_id: string
message: string
Expand Down Expand Up @@ -199,10 +215,15 @@ export interface Message {
dispatchedAt?: string
sentAt?: string
deliveredAt?: string
/** When the message failed. Present only while status is `failed`. */
failedAt?: string
receivedAt?: string
/** Current failure code. Present only while status is `failed` (test accounts can keep `FCM_SEND_SKIPPED`). */
errorCode?: string
/** Current failure text. Present only while status is `failed`, or as a timeout note on `unknown`. */
errorMessage?: string
/** Every failure the message had, plus delivery reports that did not confirm delivery (`deliveryReport: true`, not failures). Oldest first. At most 15: the first 5 and the newest 10. */
issueHistory?: MessageIssueEntry[]
simSubscriptionId?: number
smsBatch?: string
createdAt?: string
Expand Down
20 changes: 20 additions & 0 deletions test/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Textbee, TextbeeError } from '../src/index'
import type { MessageIssueEntry } from '../src/index'

const pkg = JSON.parse(
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
Expand Down Expand Up @@ -252,6 +253,25 @@ describe('response envelopes', () => {
expect(url.searchParams.get('type')).toBeNull()
})

it('types issueHistory on a message that failed and later succeeded', async () => {
const entry: MessageIssueEntry = {
code: 'FCM_SEND_FAILED',
message: 'push failed',
at: '2026-10-01T10:00:00.000Z',
source: 'push',
backfilled: true,
}
const page = {
data: [{ _id: 'sms-2', message: 'hi', type: 'SENT', status: 'sent', issueHistory: [entry] }],
meta: {},
}
fetchMock.mockImplementation(async () => respond(200, page))

const result = await client().getMessages()

expect(result.data[0]?.issueHistory).toEqual([entry])
})

it('sends no query at all for a bare account-level getMessages()', async () => {
fetchMock.mockImplementation(async () => respond(200, { data: [], meta: {} }))

Expand Down
Loading