From 7bbad925f5dc2c0a05d0b0f87fde155cf00b7bbe Mon Sep 17 00:00:00 2001 From: flovoice53-tech Date: Wed, 2 Sep 2026 09:03:10 +0300 Subject: [PATCH 1/2] New Components - sms_florin --- components/sms_florin/README.md | 27 ++++++ .../actions/get-rental/get-rental.mjs | 31 +++++++ .../actions/rent-number/rent-number.mjs | 40 +++++++++ components/sms_florin/package.json | 18 ++++ components/sms_florin/sms_florin.app.mjs | 86 +++++++++++++++++++ .../sms_florin/sources/new-sms/new-sms.mjs | 84 ++++++++++++++++++ .../sms_florin/sources/new-sms/test-event.mjs | 11 +++ 7 files changed, 297 insertions(+) create mode 100644 components/sms_florin/README.md create mode 100644 components/sms_florin/actions/get-rental/get-rental.mjs create mode 100644 components/sms_florin/actions/rent-number/rent-number.mjs create mode 100644 components/sms_florin/package.json create mode 100644 components/sms_florin/sms_florin.app.mjs create mode 100644 components/sms_florin/sources/new-sms/new-sms.mjs create mode 100644 components/sms_florin/sources/new-sms/test-event.mjs diff --git a/components/sms_florin/README.md b/components/sms_florin/README.md new file mode 100644 index 0000000000000..401af541621c8 --- /dev/null +++ b/components/sms_florin/README.md @@ -0,0 +1,27 @@ +# Overview + +The [sms-florin](https://flo-voice1.com) API lets you rent real UK mobile numbers and receive SMS/OTP verification codes on them programmatically — handy for testing signup and verification flows without burning a personal number. + +With these components you can: + +- Rent a number for a specific service (WhatsApp, Telegram, Google, and more) +- Read a rental's status, phone number, and any codes received +- Trigger a workflow the moment a code lands on one of your numbers + +# Example Use Cases + +- **End-to-end signup test** – *Rent a Number* for the service under test, drive the signup in another step, then *Get Rental* to pull the verification code. +- **Route incoming codes** – use the *New SMS Received* trigger to forward every code to Slack, a database, or a test runner. +- **Scheduled account provisioning** – on a timer, rent a number, wait for the activation code, and hand both to a downstream system. + +# Getting Started + +1. Create an account at [flo-voice1.com](https://flo-voice1.com) and add balance. +2. Generate an API key on the [API access page](https://flo-voice1.com/api-access). +3. In Pipedream, add the sms-florin app and paste the key when prompted. + +# Troubleshooting + +- **`invalid or missing API key`** – the key is wrong or was revoked. Generate a new one on the API access page and update the connected account in Pipedream. +- **`Rent a Number` fails with an insufficient-balance error** – top up your account balance; each rental is charged up front. +- **`New SMS Received` emits nothing** – a code only appears after the target service actually sends one to the rented number, and the trigger only looks at your 25 most recent rentals. diff --git a/components/sms_florin/actions/get-rental/get-rental.mjs b/components/sms_florin/actions/get-rental/get-rental.mjs new file mode 100644 index 0000000000000..5b91ef4ba8a8a --- /dev/null +++ b/components/sms_florin/actions/get-rental/get-rental.mjs @@ -0,0 +1,31 @@ +import app from "../../sms_florin.app.mjs"; + +export default { + key: "sms_florin-get-rental", + name: "Get Rental", + description: "Retrieve a rental's status, phone number, and any SMS received so far. [See the documentation](https://flo-voice1.com/api-access).", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + props: { + app, + rentalId: { + propDefinition: [ + app, + "rentalId", + ], + }, + }, + async run({ $ }) { + const rental = await this.app.getRental({ + $, + rentalId: this.rentalId, + }); + $.export("$summary", `Retrieved rental ${this.rentalId} (status: ${rental.status})`); + return rental; + }, +}; diff --git a/components/sms_florin/actions/rent-number/rent-number.mjs b/components/sms_florin/actions/rent-number/rent-number.mjs new file mode 100644 index 0000000000000..b6a99d551f52e --- /dev/null +++ b/components/sms_florin/actions/rent-number/rent-number.mjs @@ -0,0 +1,40 @@ +import app from "../../sms_florin.app.mjs"; + +export default { + key: "sms_florin-rent-number", + name: "Rent a Number", + description: "Rent a phone number for a service, debiting your account balance. [See the documentation](https://flo-voice1.com/api-access).", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + app, + serviceSlug: { + propDefinition: [ + app, + "serviceSlug", + ], + }, + period: { + propDefinition: [ + app, + "period", + ], + }, + }, + async run({ $ }) { + const response = await this.app.rentNumber({ + $, + data: { + serviceSlug: this.serviceSlug, + period: this.period, + }, + }); + $.export("$summary", `Successfully rented a number (rental ID ${response.rentalId})`); + return response; + }, +}; diff --git a/components/sms_florin/package.json b/components/sms_florin/package.json new file mode 100644 index 0000000000000..5f3128c4110c9 --- /dev/null +++ b/components/sms_florin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/sms_florin", + "version": "0.0.1", + "description": "Pipedream sms-florin Components", + "main": "sms_florin.app.mjs", + "keywords": [ + "pipedream", + "sms_florin" + ], + "homepage": "https://pipedream.com/apps/sms_florin", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" + } +} diff --git a/components/sms_florin/sms_florin.app.mjs b/components/sms_florin/sms_florin.app.mjs new file mode 100644 index 0000000000000..20ccc507b4ba6 --- /dev/null +++ b/components/sms_florin/sms_florin.app.mjs @@ -0,0 +1,86 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "sms_florin", + propDefinitions: { + serviceSlug: { + type: "string", + label: "Service", + description: "The service to rent a number for (e.g. `whatsapp`, `telegram`, `google`).", + async options() { + const { services } = await this.listServices(); + return services?.map(({ + slug, name, + }) => ({ + label: name, + value: slug, + })) || []; + }, + }, + period: { + type: "string", + label: "Period", + description: "How long to hold the number. `instant` is a short rental for a single code; `monthly` keeps the number for 30 days.", + options: [ + "instant", + "monthly", + ], + default: "instant", + }, + rentalId: { + type: "integer", + label: "Rental ID", + description: "The ID of a rental, as returned by **Rent a Number**.", + }, + }, + methods: { + _baseUrl() { + return "https://flo-voice1.com/api/v1"; + }, + async _makeRequest({ + $, path, ...opts + }) { + return axios($ || this, { + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_key}`, + }, + ...opts, + }); + }, + async listServices(opts = {}) { + return this._makeRequest({ + path: "/services", + ...opts, + }); + }, + async rentNumber({ + data, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: "/rentals", + data, + ...opts, + }); + }, + async listRentals({ + params, ...opts + } = {}) { + return this._makeRequest({ + path: "/rentals", + params, + ...opts, + }); + }, + async getRental({ + rentalId, ...opts + }) { + return this._makeRequest({ + path: `/rentals/${rentalId}`, + ...opts, + }); + }, + }, +}; diff --git a/components/sms_florin/sources/new-sms/new-sms.mjs b/components/sms_florin/sources/new-sms/new-sms.mjs new file mode 100644 index 0000000000000..15d71c2956cf4 --- /dev/null +++ b/components/sms_florin/sources/new-sms/new-sms.mjs @@ -0,0 +1,84 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../sms_florin.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "sms_florin-new-sms", + name: "New SMS Received", + description: "Emit a new event each time an SMS arrives on one of your rented numbers.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async getNewMessages(lastId) { + const { rentals } = await this.app.listRentals({ + params: { + limit: 25, + }, + }); + const events = []; + for (const rental of rentals || []) { + for (const message of rental.messages || []) { + if (message.id > lastId) { + events.push({ + rental, + message, + }); + } + } + } + // Oldest first, so events are emitted in chronological order and the + // greatest id is the last one persisted. + return events.sort((a, b) => a.message.id - b.message.id); + }, + emitEvent({ + rental, message, + }) { + this.$emit({ + ...message, + rentalId: rental.id, + service: rental.service?.name || null, + serviceSlug: rental.service?.slug || null, + phoneNumber: rental.phoneNumber, + country: rental.country, + }, { + id: message.id, + summary: `New SMS on ${rental.phoneNumber || rental.service?.name || "rented number"}`, + ts: Date.parse(message.receivedAt) || Date.now(), + }); + }, + }, + hooks: { + async deploy() { + // Don't replay codes that arrived before the source was set up — just + // record the current high-water mark. + const events = await this.getNewMessages(0); + const lastId = events.at(-1)?.message?.id || 0; + this._setLastId(lastId); + }, + }, + async run() { + const events = await this.getNewMessages(this._getLastId()); + for (const event of events) { + this.emitEvent(event); + this._setLastId(event.message.id); + } + }, + sampleEmit, +}; diff --git a/components/sms_florin/sources/new-sms/test-event.mjs b/components/sms_florin/sources/new-sms/test-event.mjs new file mode 100644 index 0000000000000..f908b3022ea31 --- /dev/null +++ b/components/sms_florin/sources/new-sms/test-event.mjs @@ -0,0 +1,11 @@ +export default { + "id": 1024, + "sender": "WhatsApp", + "body": "Your WhatsApp code is 123-456", + "receivedAt": "2026-08-07T12:00:00.000Z", + "rentalId": 42, + "service": "WhatsApp", + "serviceSlug": "whatsapp", + "phoneNumber": "+447700900123", + "country": "GB" +} From 191d7234c02dfd6d5698457e1765e437ca144794 Mon Sep 17 00:00:00 2001 From: flovoice53-tech Date: Wed, 2 Sep 2026 10:27:10 +0300 Subject: [PATCH 2/2] sms_florin: expand action/source descriptions, add JSDoc to app methods --- .../actions/get-rental/get-rental.mjs | 2 +- .../actions/rent-number/rent-number.mjs | 2 +- components/sms_florin/sms_florin.app.mjs | 22 +++++++++++++++++++ .../sms_florin/sources/new-sms/new-sms.mjs | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/components/sms_florin/actions/get-rental/get-rental.mjs b/components/sms_florin/actions/get-rental/get-rental.mjs index 5b91ef4ba8a8a..a056acfe54097 100644 --- a/components/sms_florin/actions/get-rental/get-rental.mjs +++ b/components/sms_florin/actions/get-rental/get-rental.mjs @@ -3,7 +3,7 @@ import app from "../../sms_florin.app.mjs"; export default { key: "sms_florin-get-rental", name: "Get Rental", - description: "Retrieve a rental's status, phone number, and any SMS received so far. [See the documentation](https://flo-voice1.com/api-access).", + description: "Fetch a point-in-time snapshot of a rental — its status, the assigned phone number and country, and every SMS received so far. The `Rental ID` comes from **Rent a Number**. Poll this (or use the **New SMS Received** source) until the code you need appears. [See the documentation](https://flo-voice1.com/api-access).", version: "0.0.1", type: "action", annotations: { diff --git a/components/sms_florin/actions/rent-number/rent-number.mjs b/components/sms_florin/actions/rent-number/rent-number.mjs index b6a99d551f52e..a998bf66d4b67 100644 --- a/components/sms_florin/actions/rent-number/rent-number.mjs +++ b/components/sms_florin/actions/rent-number/rent-number.mjs @@ -3,7 +3,7 @@ import app from "../../sms_florin.app.mjs"; export default { key: "sms_florin-rent-number", name: "Rent a Number", - description: "Rent a phone number for a service, debiting your account balance. [See the documentation](https://flo-voice1.com/api-access).", + description: "Rent a phone number for a service so you can receive its SMS verification code. Choose a `Service` from the list and a `Period` — `instant` holds the number briefly for a single code, `monthly` keeps it for 30 days. Your account balance is debited immediately. Pass the returned `rentalId` to **Get Rental** to read the code. [See the documentation](https://flo-voice1.com/api-access).", version: "0.0.1", type: "action", annotations: { diff --git a/components/sms_florin/sms_florin.app.mjs b/components/sms_florin/sms_florin.app.mjs index 20ccc507b4ba6..ebfd43dd216f7 100644 --- a/components/sms_florin/sms_florin.app.mjs +++ b/components/sms_florin/sms_florin.app.mjs @@ -49,12 +49,22 @@ export default { ...opts, }); }, + /** + * List the services a number can be rented for, with their prices. + * @returns {Promise<{ services: object[] }>} Each service has `slug`, `name`, `basePriceCents` and `monthlyPriceCents`. + */ async listServices(opts = {}) { return this._makeRequest({ path: "/services", ...opts, }); }, + /** + * Rent a number for a service, debiting the account balance. + * @param {object} args + * @param {{ serviceSlug: string, period: "instant"|"monthly" }} args.data - The rental request. + * @returns {Promise<{ rentalId: number }>} + */ async rentNumber({ data, ...opts }) { @@ -65,6 +75,12 @@ export default { ...opts, }); }, + /** + * List the account's rentals, newest first, each with its received messages. + * @param {object} [args] + * @param {{ limit?: number }} [args.params] - `limit` caps how many rentals are returned. + * @returns {Promise<{ rentals: object[] }>} + */ async listRentals({ params, ...opts } = {}) { @@ -74,6 +90,12 @@ export default { ...opts, }); }, + /** + * Get a single rental's current status, phone number and messages. + * @param {object} args + * @param {number} args.rentalId - The rental to fetch. + * @returns {Promise} + */ async getRental({ rentalId, ...opts }) { diff --git a/components/sms_florin/sources/new-sms/new-sms.mjs b/components/sms_florin/sources/new-sms/new-sms.mjs index 15d71c2956cf4..cb7b4b72f98a3 100644 --- a/components/sms_florin/sources/new-sms/new-sms.mjs +++ b/components/sms_florin/sources/new-sms/new-sms.mjs @@ -5,7 +5,7 @@ import sampleEmit from "./test-event.mjs"; export default { key: "sms_florin-new-sms", name: "New SMS Received", - description: "Emit a new event each time an SMS arrives on one of your rented numbers.", + description: "Emit a new event each time an SMS arrives on one of your rented numbers. Polls your 25 most recent rentals on a timer (default every 15 minutes); codes only land on recent, active rentals, so older ones are intentionally out of scope. On deploy it records the current position without replaying past codes. No configuration beyond the connected account. [See the documentation](https://flo-voice1.com/api-access).", version: "0.0.1", type: "source", dedupe: "unique",