A reliable connector has five parts: a scheduler that runs poll jobs, a webhook receiver, a set of flow workers, a state store, and a retry queue. Every part reads from and writes to the state store, which is what lets the connector stop and restart without losing its place.
Webhooks and the scheduler both feed the same flow workers; every worker reads and writes the state store, and failures go to the retry queue.
Webhooks and the scheduler both feed the same flow workers; every worker reads and writes the state store, and failures go to the retry queue.
| Part | Responsibility |
|---|---|
| Settings loader | Reads your integration record with the Connector API Key and keeps the on/off value of each flow up to date |
| Scheduler | Runs each enabled poll job on an interval, for example every 5 minutes. Never runs two copies of the same job at once |
| Webhook receiver | An HTTPS endpoint that accepts Pipe17 webhook calls, records them, and replies immediately |
| Flow workers | One per flow (Chapter 7). Each reads a change, maps it, and writes it to the other side |
| State store | Cursors, ID mappings and processed-event keys (Chapter 4) |
| Retry queue | Holds work that failed, with the next attempt time (Chapter 8) |
You can host these as one process (a small Node.js service with a cron library), or split them across serverless functions with a managed queue and database. The design below works the same either way.
One connector, many integrations
Think of the connector as a class definition and each integration as an object instance with its own state. You define the connector once. Every customer organization that uses it gets its own integration, identified by the pair integrationId + orgKey. Your code must handle everything in those pairs.
| Belongs to the connector (one for all customers) | Belongs to each integration (one per customer) |
|---|---|
Connector API Key and connectorId
|
integrationId + orgKey
|
| Flow list, mappings, connection field and setting definitions | Integration API Key |
| Your code and your hosting | Flow toggles and setting values |
| Connection field values (the customer's credentials for your system) | |
| Cursors, ID mappings, processed events, retry queue | |
| Webhook subscription |
In practice:
-
Keep a registry of integrations. List your connector's integrations with the Connector API Key (
GET /integrations); each comes back with itsintegrationId,orgKey,statusandsettings. Refresh the registry on a schedule and when theintegrationswebhook fires. -
Store one Integration API Key per pair in your secrets manager, for example under
pipe17/<orgKey>/<integrationId>. The customer's admin finds it on the integration's API Key tab. - Run every job once per integration. The scheduler loops over active integrations and passes the pair and its key to each job. Never let one customer's job read another's settings, cursors or key.
-
Scope all state by the pair. Every table in Chapter 4 carries
org_keyandintegration_id. -
Route webhooks by
orgKey. Each integration registers its own webhook with its own key. Every delivery carriesorgKey, which tells you whose integration it belongs to.
// scheduler.js – run each job for every active integration
import { refreshSettings, isFlowEnabled } from './settings.js';
import { pollShippingRequests } from './flows/shippingRequests.js';
export async function runAll() {
const integrations = await refreshSettings(); // GET /integrations with the Connector API Key
for (const integ of integrations.filter((i) => i.status === 'connected')) {
const ctx = {
orgKey: integ.orgKey,
integrationId: integ.integrationId,
key: await secrets.get(`pipe17/${integ.orgKey}/${integ.integrationId}`), // your secrets manager
settings: integ.settings,
};
if (isFlowEnabled(ctx, 'shipments.out')) await pollShippingRequests(ctx);
// ...the other flows, each with the same ctx
}
}Every job and handler in the rest of this guide takes this ctx as its first argument. To keep the examples short, they leave out two things your connector must add: key: ctx.key on every API call (p17(method, path, { key: ctx.key, ... })) and ctx.orgKey + ctx.integrationId on every state lookup.
Placeholders. The examples call a few things you write yourself: mySystem.* (your system's API), state.* (your data-access module, Chapter 4), secrets (your secrets manager), contextFor() (looks up an integration in your registry), chunk() (splits a list into batches) and reportException() (Chapter 8).
Honor the flow settings
An admin turns flows on and off on the integration's Settings tab. Your connector reads them from settings.fields, a list of { path, value } entries where path is the flow name (shipments.out, fulfillments.in, inventory.in, products.out, locations.in). A flow that is off or missing must not run.
// settings.js – load every integration of your connector and read its settings
import { p17 } from './p17.js';
export async function refreshSettings() {
const res = await p17('GET', '/integrations', {
key: process.env.PIPE17_CONNECTOR_KEY,
query: { keys: 'integrationId,orgKey,status,settings,connection' },
});
return res.integrations ?? [];
}
// ctx = { orgKey, integrationId, key, settings } for one integration (see above)
export function isFlowEnabled(ctx, path) {
return ctx.settings?.fields?.find((f) => f.path === path)?.value === true;
}Call refreshSettings() at startup and on a schedule (every 5 minutes is enough). Subscribe to the integrations webhook topic to pick up changes sooner. Check isFlowEnabled() at the start of every job and before handling every webhook.
Also check the integration's status. If it is disabled or error, pause all flows until it returns to connected.
Add your own settings
Use custom settings for anything that differs from one customer to the next: a SKU filter, a default shipping method, a warehouse code. Each customer then sets their own values on the integration's Settings tab, and one connector serves them all. The example below adds a productSKUs list that the products flow (7.2) uses as a filter. A setting has two halves:
| Half | Who sets it | Where it lives |
|---|---|---|
| Definition: the field's name, type, label and help text | You, once, for all customers | The connector: settings.fields
|
| Value: this customer's SKU list | Each customer's admin, on the Settings tab | Each integration: settings.fields as { path, value }
|
Step 1 – Read the current settings definition. The list already holds your flow toggles (products.out, shipments.out…), and you must send them back unchanged.
curl -s -G "$PIPE17_BASE_URL/connectors/<connectorId>" \ -H "X-Pipe17-Key: $PIPE17_CONNECTOR_KEY" \ --data-urlencode "keys=settings"
Step 2 – Add the new field and send the complete list back. Treat settings.fields like entities: the PUT replaces the whole list, so always send every field, existing toggles included.
curl -s -X PUT "$PIPE17_BASE_URL/connectors/<connectorId>" \
-H "Content-Type: application/json" \
-H "X-Pipe17-Key: $PIPE17_CONNECTOR_KEY" \
-d '{
"settings": {
"fields": [
{
"path": "products.out",
"type": "boolean",
"options": { "label": "Send products to the warehouse", "component": "switch", "defaultValue": false }
},
"… the other existing fields from Step 1, unchanged …",
{
"path": "productSKUs",
"type": "array",
"options": {
"label": "Process only these SKUs",
"helperText": "Optional. Exact SKUs to send to the warehouse. Empty means all active products.",
"component": "text-array",
"defaultValue": []
}
}
]
}
}'Replace the quoted placeholder line with the real field objects from Step 1 before sending.
| Field property | Meaning |
|---|---|
path |
The setting's key. Your connector looks it up by this name |
type |
string, number, boolean, date, array, object or json
|
secret |
true to mask the value in the app |
options.label / options.helperText
|
The field's title and the hint shown under it |
options.component |
How the field is drawn, for example switch for a toggle or text-array for a list of values |
options.defaultValue |
The value a new integration starts with |
options.values |
A fixed list of { label, value } choices, for a dropdown |
validation |
Optional rules: required, minimum, maximum, minLength, maxLength or regex, each with a message
|
Step 3 – Each customer fills in their value. The new field appears on the Settings tab of every integration of your connector. An existing integration may need an upgrade before the field shows (see Versions and upgrades in Chapter 2). Once saved, the customer's value is on their integration record:
curl -s -G "$PIPE17_BASE_URL/integrations" \ -H "X-Pipe17-Key: $PIPE17_CONNECTOR_KEY" \ --data-urlencode "keys=integrationId,settings"
{
"integrationId": "<integrationId>",
"settings": {
"fields": [
{ "path": "products.out", "value": true },
{ "path": "productSKUs", "value": ["TSHIRT-BLK-M", "TSHIRT-BLK-L"] }
]
}
}Step 4 – Your connector applies it. Read the value with getSetting(ctx, 'productSKUs') (defined below) and pass it to the products call as sku. Because the value belongs to each integration, one connector can serve many customers, each with its own list. The same pattern works for any per-customer option, for example an inventorySKUs filter for the inventory flow or a default shipping method.
// settings.js – add next to isFlowEnabled()
export function getSetting(ctx, path) {
return ctx.settings?.fields?.find((f) => f.path === path)?.value;
}Troubleshooting
-
The integration runs but no data syncs
The settings for each entity flow are off by default. On the integration's Settings tab, turn on the flows your integration supports, and confirm your code reads the latest settings. -
A workflow keeps running after an admin turns it off
Your code may be using cached settings. Poll for integration updates with theupdatedSincefilter, and refresh your stored settings before each job. -
API calls fail with rate-limit errors
Reduce how often your jobs run, or add retries with backoff.
Need Help?
If you need additional assistance:
- Use Ask Pippen, our AI agent, located at the top of the app page.
- Submit a support request with as much relevant detail as possible. Learn how to submit a request.
We're here to help you succeed with your operations.
Comments
0 comments