Your connector needs four small tables: cursors, ID mappings, processed events, and a retry queue. Without them it cannot tell what is new, which record on the other side to update, or whether it already did a piece of work.
| State | Answers the question | Example |
|---|---|---|
| Cursors | Where did the last poll of this stream stop? |
shipping_requests.ready → 2026-10-12T06:55:00Z
|
| ID mappings | Which record in my system matches this Pipe17 record, and what status did I last see? | Shipping Request abc123 ↔ warehouse order WO-88812, status sentToFulfillment
|
| Processed events | Have I already handled this exact change or webhook? | shipments:abc123:v7 |
| Retry queue | What failed, why, and when should I try again? | Create fulfillment for WO-88812, attempt 3, next try in 4 minutes |
A schema that covers all four (SQLite or Postgres syntax):
-- Every table is scoped to one integration: (org_key, integration_id) -- Where each poll stream left off CREATE TABLE cursors ( org_key TEXT NOT NULL, integration_id TEXT NOT NULL, stream TEXT NOT NULL, -- e.g. 'shipping_requests.ready', 'products' updated_since TEXT NOT NULL, -- RFC 3339 timestamp last_run_at TEXT, PRIMARY KEY (org_key, integration_id, stream) ); -- Pipe17 record <-> your record CREATE TABLE id_map ( org_key TEXT NOT NULL, integration_id TEXT NOT NULL, entity TEXT NOT NULL, -- 'shipments', 'locations', 'products', 'fulfillments' pipe17_id TEXT, ext_id TEXT, last_status TEXT, -- last status you saw or set last_version TEXT, -- Pipe17 version or updatedAt you last processed data TEXT, -- optional JSON snapshot for diffing updated_at TEXT NOT NULL, PRIMARY KEY (org_key, integration_id, entity, pipe17_id), UNIQUE (org_key, integration_id, entity, ext_id) ); -- Idempotency: one row per change you have fully handled CREATE TABLE processed ( org_key TEXT NOT NULL, integration_id TEXT NOT NULL, event_key TEXT NOT NULL, processed_at TEXT NOT NULL, PRIMARY KEY (org_key, integration_id, event_key) ); -- Work that failed and must be retried CREATE TABLE retry_queue ( id INTEGER PRIMARY KEY, org_key TEXT NOT NULL, integration_id TEXT NOT NULL, job_type TEXT NOT NULL, -- 'pushFulfillment', 'sendShippingRequest', ... payload TEXT NOT NULL, -- JSON attempts INTEGER NOT NULL DEFAULT 0, next_try_at TEXT NOT NULL, last_error TEXT );
Rules that keep state correct
- Advance a cursor only after the whole window is handled. Items that failed go to the retry queue, so the cursor can still move forward.
-
Write the mapping before you call the other side, then complete it. For example, insert
id_mapwithpipe17_idand noext_id, create the order in your system, then saveext_id. If the process crashes in between, the half-filled row tells you to check your system before creating again. -
Make every write idempotent. Build an event key from the record ID plus its
version(orupdatedAt), skip keys already inprocessed, and insert the key only after the work succeeds. -
Prefer lookups over assumptions. Before creating a Fulfillment, look it up by extFulfillmentId; before creating a Location, look it up in
id_map. Pipe17 rejects duplicate keys with HTTP 409, which you can treat as "already done". - Keep secrets out of the state store. Store API keys in your secrets manager, not alongside cursors.
A thin data-access module keeps the rest of the code simple. The examples in later chapters assume these functions exist: getCursor(stream), setCursor(stream, ts), getMapping(entity, pipe17Id), findByExtId(entity, extId), findByPipe17Id(entity, pipe17Id), saveMapping(entity, row), wasProcessed(key), markProcessed(key) and enqueueRetry(jobType, payload, error).
Troubleshooting
-
Locations don't appear in the app
Confirm that thelocations.insetting is turned on and that your job runs without errors. Check the job's task status for failures. -
Duplicate locations appear after each run
Your code is creating locations instead of updating them. Store the location ID the app returns, or check the List locations operation before you create a location. -
Orders aren't routed to your locations
Confirm thatfulfillmentIntegrationIdon each location is set to your integration ID. In the app, the location's 3PL Integration must show your custom integration.
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