Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions components/sms_florin/README.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions components/sms_florin/actions/get-rental/get-rental.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import app from "../../sms_florin.app.mjs";

export default {
key: "sms_florin-get-rental",
name: "Get Rental",
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: {
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;
},
};
40 changes: 40 additions & 0 deletions components/sms_florin/actions/rent-number/rent-number.mjs
Original file line number Diff line number Diff line change
@@ -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 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: {
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;
},
};
18 changes: 18 additions & 0 deletions components/sms_florin/package.json
Original file line number Diff line number Diff line change
@@ -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 <support@pipedream.com> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "^3.1.0"
}
}
108 changes: 108 additions & 0 deletions components/sms_florin/sms_florin.app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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,
});
},
/**
* 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
}) {
return this._makeRequest({
method: "POST",
path: "/rentals",
data,
...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
} = {}) {
return this._makeRequest({
path: "/rentals",
params,
...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<object>}
*/
async getRental({
rentalId, ...opts
}) {
return this._makeRequest({
path: `/rentals/${rentalId}`,
...opts,
});
},
},
};
84 changes: 84 additions & 0 deletions components/sms_florin/sources/new-sms/new-sms.mjs
Original file line number Diff line number Diff line change
@@ -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. 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",
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,
};
11 changes: 11 additions & 0 deletions components/sms_florin/sources/new-sms/test-event.mjs
Original file line number Diff line number Diff line change
@@ -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"
}