From 507db792277b5ec68b8a6c317f8977b0cb4eedeb Mon Sep 17 00:00:00 2001 From: isra el Date: Tue, 10 Nov 2026 17:19:28 +0300 Subject: [PATCH] feat: add simSlotIndex to sendSms Choose the SIM by its slot (0 is SIM 1). simSubscriptionId is deprecated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017B6pTTFVhKWwcKLit7MEdz --- README.md | 15 ++++++++++----- src/client.ts | 3 +++ src/types.ts | 28 +++++++++++++++++++++------- test/client.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 271ffa2..df317f2 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ await textbee.sendSms({ | Option | Type | What it does | | --- | --- | --- | | `deviceId` | `string` | Which phone sends the message. Omit it and textbee uses your default device, or the enabled device with the most recent heartbeat. | -| `simSubscriptionId` | `number` | Which SIM sends the message on a multi-SIM phone. Omit it and the phone uses its configured preferred SIM, or the system default. | +| `simSlotIndex` | `number` | Which SIM sends the message, by its slot in the phone. `0` is SIM 1, `1` is SIM 2. Omit it and the phone uses its default SIM. | +| `simSubscriptionId` | `number` | Deprecated. Use `simSlotIndex`. See [Choosing a SIM](#choosing-a-sim). | | `scheduledAt` | `string \| Date` | Send later instead of now. ISO 8601 or a `Date`, must be in the future. | ```js @@ -50,16 +51,20 @@ await textbee.sendSms({ recipients: ['+12025550123'], message: 'Your appointment is tomorrow at 9am', deviceId: '65f0000000000000000000aa', - simSubscriptionId: 2, + simSlotIndex: 1, scheduledAt: new Date(Date.now() + 60 * 60 * 1000), }) ``` -### Finding your simSubscriptionId +### Choosing a SIM -Open the textbee Android app, go to **Dashboard**, and find the **SIM Cards** section. Each SIM shows its subscription id with a copy button. +Use `simSlotIndex: 0` for SIM 1 and `simSlotIndex: 1` for SIM 2. The slot is the place of the SIM in the phone. It stays the same when you take the SIM out and put it back. -Be aware that this value is not validated. If the id does not match a SIM currently in the phone it is ignored, and the message goes out from the preferred or default SIM instead. Nothing errors, so confirm which SIM was used by checking the number the message arrived from. +The slot is the place in the phone, not a specific card. A different card in that slot sends from the new card. + +It works with or without `deviceId`. If the slot has no active SIM, the message fails with the error code `SIM_SLOT_EMPTY`. textbee never sends it from another SIM. With textbee app versions before slot support, the server refuses a slot it cannot map from the phone's last SIM report with HTTP 400, so the call throws. + +`simSubscriptionId` still works, but it is deprecated. It is an Android id that changes when a SIM is reinserted. A stale id sends from the default SIM and nothing errors. If you send both options, textbee uses `simSlotIndex` and ignores the id. ## Devices diff --git a/src/client.ts b/src/client.ts index 4b2b324..764ac7d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -45,6 +45,9 @@ export class Textbee { if (request.deviceId !== undefined) { body.deviceId = request.deviceId } + if (request.simSlotIndex !== undefined) { + body.simSlotIndex = request.simSlotIndex + } if (request.simSubscriptionId !== undefined) { body.simSubscriptionId = request.simSubscriptionId } diff --git a/src/types.ts b/src/types.ts index 1d13e8b..ddd1c5f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,15 +37,27 @@ export interface SendSmsRequest { deviceId?: string /** - * Which SIM sends the message on a multi-SIM phone. + * Which SIM sends the message, by its slot in the phone. `0` is SIM 1 and + * `1` is SIM 2. Works with or without `deviceId`. * - * This value is not validated. An id that does not match a SIM currently in - * the phone is ignored, and the message goes out from the SIM configured in - * the app's settings, or the system default. Nothing errors, so confirm the - * SIM by checking which number the message arrived from. + * If the slot has no active SIM, the message fails with the error code + * `SIM_SLOT_EMPTY`. It is never sent from another SIM. Omit it and the phone + * uses its default SIM. * - * Find the id in the textbee Android app under Dashboard, "SIM Cards", where - * each SIM shows its number with a copy button. + * The slot is the place in the phone, not a specific card: a different card + * in that slot sends from the new card. With textbee app versions before slot + * support, the server refuses a slot it cannot map from the phone's last SIM + * report with HTTP 400, so the call throws. + */ + simSlotIndex?: number + + /** + * Which SIM sends the message, by its Android subscription id. + * + * @deprecated Use `simSlotIndex`. Android changes this id when a SIM is + * reinserted, and a stale id sends from the default SIM with no error. It + * still works. If you send both, textbee uses `simSlotIndex` and ignores + * this value. */ simSubscriptionId?: number @@ -229,6 +241,8 @@ export interface Message { 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[] + /** The SIM slot the sender asked for. `0` is SIM 1, `1` is SIM 2. */ + simSlotIndex?: number simSubscriptionId?: number smsBatch?: string createdAt?: string diff --git a/test/client.test.ts b/test/client.test.ts index cc2f108..394f501 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -92,6 +92,43 @@ describe('sendSms', () => { } }) + it('passes simSlotIndex through, including 0', async () => { + fetchMock.mockImplementation(async () => respond(200, { data: { successCount: 1 } })) + + await client().sendSms({ message: 'a', recipients: ['+15555550123'], simSlotIndex: 0 }) + expect(lastCall().body).toEqual({ + message: 'a', + recipients: ['+15555550123'], + simSlotIndex: 0, + }) + + await client().sendSms({ message: 'b', recipients: ['+15555550123'], simSlotIndex: 1 }) + expect(lastCall().body.simSlotIndex).toBe(1) + }) + + it('sends both simSlotIndex and simSubscriptionId when both are given', async () => { + fetchMock.mockImplementation(async () => respond(200, { data: { successCount: 1 } })) + + await client().sendSms({ + message: 'both', + recipients: ['+15555550123'], + simSlotIndex: 1, + simSubscriptionId: 2, + }) + + expect(lastCall().body).toMatchObject({ simSlotIndex: 1, simSubscriptionId: 2 }) + }) + + it('omits simSlotIndex when neither SIM option is given', async () => { + fetchMock.mockImplementation(async () => respond(200, { data: { successCount: 1 } })) + + await client().sendSms({ message: 'none', recipients: ['+15555550123'] }) + + const keys = Object.keys(lastCall().body) + expect(keys).not.toContain('simSlotIndex') + expect(keys).not.toContain('simSubscriptionId') + }) + it('passes optional fields through and omits them entirely when unset', async () => { fetchMock.mockImplementation(async () => respond(200, { data: { successCount: 1 } })) @@ -113,6 +150,7 @@ describe('sendSms', () => { const keys = Object.keys(lastCall().body) expect(keys).not.toContain('deviceId') + expect(keys).not.toContain('simSlotIndex') expect(keys).not.toContain('simSubscriptionId') expect(keys).not.toContain('scheduledAt') })