Skip to content
adapters.io

Square to Postgres: how to replicate payments data your app can actually query

9 min read Data engineering The Adapters team

Last updated July 2026

Field mapping auto-plugged · tap a port to rewire

5 sample records ready

To replicate Square into Postgres, read the objects you care about (Payments, Orders, Refunds, Customers, Payouts) through the Square API, page each list with cursor pagination, and filter every incremental run on the updated-at range. Flatten the nested total_money object (an integer amount in the smallest currency unit plus a currency code) into a money column stored as NUMERIC(19,4) or as integer cents, then upsert with INSERT ... ON CONFLICT (id) DO UPDATE keyed on the Square payment id. Your app and reports then query the Postgres copy instead of calling Square live.

Key takeaways

  • Page with cursors, not offsets. Square list endpoints return an opaque cursor; you pass it back to get the next page and stop when no cursor comes back. There is no page number to jump to.
  • Money is integer cents in a nested object. total_money carries an amount in the smallest currency unit plus a three-letter currency, and a seller can run multiple locations in multiple currencies, so store both.
  • Watermark on updated-at, reconcile refunds and disputes. Pull only records changed since the last run, and remember that a settled payment can still be refunded or disputed, so it mutates after the fact.
  • Upsert on the Square id is idempotent. ON CONFLICT (id) DO UPDATE makes a retried batch or an overlapping window update the row in place instead of doubling it.

How do you connect Square to Postgres?

You connect Square to Postgres by reading Square objects through the Square API and writing them into Postgres tables, because Square gives you no database endpoint to point psql at. Something has to sit in the middle: a managed connector, or a scheduled job that authenticates with an access token, lists each object type, pages through the cursors, and upserts every batch into your schema on the Square id.

The connection has three parts. First, credentials: a Square application with an OAuth access token (or a personal access token for a single seller) scoped to the payments, orders, and customers read permissions, plus the merchant and location ids you are reading. Square runs a sandbox and a production environment on separate base URLs and separate tokens, so build against sandbox and cut over to production once the schema is stable. Second, a Postgres landing spot: a database, a schema, and a role that can create tables and run upserts. Third, the transfer loop itself, which walks each list endpoint by cursor, lands each page in staging, and upserts into the target. The managed path is the Square to Postgres integration, which owns the token refresh, the cursor paging, the updated-at watermark, the staging tables, the upserts, and the backoff.

Method What it is When it fits
Full reload Walk every object type end to end by cursor with no date filter, truncate, and reload the target The first backfill, and tiny reference sets like locations. Wasteful to rerun on payments and orders that barely change between runs
Updated-at watermark The same cursor walk, but filtered so each run only returns records with updated_at after your stored maximum The default for steady-state incremental reads. Cheap and fast, but you must overlap the window and reconcile deletes and refunds separately
Webhooks Square POSTs an event (for example payment.updated or refund.updated) to your endpoint when something changes Near real time refresh hints. Best-effort delivery only: verify the signature, treat each event as a nudge to re-read the object, never as the source of truth
Managed sync tool A hosted connector that owns tokens, cursor paging, throttling, and idempotent loads When you would rather not maintain token rotation and refund reconciliation. Less control over exact field selection

Most production setups combine three of these: a full reload for the initial backfill, the updated-at watermark for the steady state, and webhooks to shorten latency and to hint at refunds and disputes that mutate a payment after it settled. If your reporting lives in a warehouse rather than an application database, the sibling path is Square to Snowflake, and the modeling discipline below carries over unchanged.

Does Square have an API to export payments data?

Yes. Square exposes a REST API with a Payments API, an Orders API, a Refunds API, a Customers API, and a Payouts API, but it is a per-object list API, not a database and not a bulk dump. You authenticate with a token, call each list endpoint, and page through results with an opaque cursor. There is no direct database connection, no read replica, and no single query that joins a payment to the order it paid for.

Each endpoint is deliberately narrow. ListPayments returns payments for a date range with a cursor; SearchOrders filters orders by location and state; the Refunds and Payouts APIs return their own object shapes. Every object carries an id, a location_id, RFC 3339 timestamps in created_at and updated_at, and money as a nested object rather than a bare number. That gap between a list API and a database is the whole reason this pipeline exists. Calling Square live from a dashboard is slow and easily throttled: results come back a page at a time, requests are rate limited, and a report that ties a payment to its order, its refunds, and the payout that settled it turns into many round trips because the API cannot join across objects. Once the data sits in Postgres, attributing revenue by location, or reconciling Square payouts against your bank feed, is one join instead of a paging loop, which is exactly what a data integration platform is for.

How Square objects map to Postgres tables and types

The one rule that matters everywhere is money. Square never sends a monetary value as a bare decimal. It sends a money object, { "amount": 1299, "currency": "USD" }, where amount is an integer in the smallest unit of that currency (cents for USD, and note that not every currency has two decimal places). Store the integer amount and the currency code together, either as integer cents plus a currency column or as NUMERIC(19,4) after dividing by the currency's minor-unit factor. Never coerce it into a floating point type: binary floats cannot hold every two-decimal cent exactly, and summing thousands of payments drifts by pennies that reconciliation will flag.

Square field Postgres table.column Type and notes
payment.id square_payment.id TEXT primary key (or BIGINT only if you map to a surrogate). The Square id is a string; keep it as TEXT and make it the upsert conflict target
order.total_money.amount square_payment.total_cents Integer cents (BIGINT), or NUMERIC(19,4) once divided by the minor-unit factor. Store the paired currency in its own column
buyer.email_address square_payment.buyer_email TEXT, nullable. Often absent on card-present sales; do not assume it is present
payment.location_id square_payment.location_id TEXT foreign key to a replicated square_location.id. The key to per-location reporting
payment.status square_payment.status TEXT. APPROVED, COMPLETED, CANCELED, or FAILED. Changes over the payment's life, so the watermark must re-read it
payment.updated_at square_payment.updated_at TIMESTAMPTZ. Parse the RFC 3339 string to UTC. The incremental watermark; index it per table
refund.id, refund.amount_money square_refund (child table) Own table keyed to payment id. A payment can have several partial refunds, so never flatten them into the payment row
payout.id square_payout.id TEXT. Ties settled payments to a bank deposit for reconciliation against the payout total

Model refunds and payment line detail as their own tables, not columns. A payment can be refunded in parts and disputed after it settled, so a refund is a separate object with its own id, amount, and status that points back at the payment. Keeping refunds in a child table lets you compute net revenue per location as a plain join and aggregate, and it means a new refund on an old payment is just an insert into the refund table plus an update to the payment row. This modeling is where a solo operator and a finance team diverge: if you are a solo seller who just needs to track what each payout actually nets you after fees and refunds, a light copy of payments and payouts is plenty; a multi-location business modeling margin wants the full object graph.

How do you sync Square incrementally instead of a full export?

Sync incrementally by filtering each list call on the updated-at range, so you only fetch records that changed since your last run. Store the maximum updated_at you saw as a watermark, resume from it next time, and overlap the window by a few minutes so a record whose timestamp landed out of clock order is never skipped. Refunds and status changes flow through the same way, because they bump the payment's updated_at.

A typical run reads the stored watermark, calls the list endpoint with a begin time a few minutes before it, and walks the cursor to the end of the range. When a page comes back it upserts the batch and tracks the largest updated_at seen, then advances the watermark once the walk finishes cleanly. Updated-at works because Square touches updated_at whenever a payment's status moves from APPROVED to COMPLETED, when a refund is created, or when a dispute changes state, so those late mutations come back on the next incremental pass without a full reload. Webhooks are the optional accelerator: a payment.updated or refund.updated event tells you to re-read one object now rather than waiting for the next scheduled run, but because delivery is best-effort you still lean on the watermark as the source of truth. The overlap is what keeps this correct, and it costs nothing because the load is idempotent, which is the next section.

How do you handle the Square API rate limit and pagination?

Handle it by paging with the cursor Square hands you and pacing requests so you stay under the rate limit instead of firing as fast as you can. Square list endpoints return an opaque cursor with each page; you pass it back on the next call and stop when the response omits it. When a throttle response comes back, back off and retry the same cursor rather than hammering the endpoint.

Cursor pagination is different from offset pagination in a way that matters. There is no page number and no way to jump to page 40; you must walk pages in order, and the cursor is opaque, so you cannot construct or cache one. Persist the cursor of the page you are on so a crash resumes mid-walk instead of restarting the whole range. Keep pages moving in a single ordered loop per object type rather than trying to parallelize one endpoint's pages, because you cannot fan out over cursors you do not have yet. On rate limits, Square returns an error you should treat as expected under load: wrap each request in retry logic with exponential backoff that recognizes the throttle, waits, and resumes from the same cursor so no page is skipped or double read. Because the steady-state watermark run only touches records that changed, you rarely approach the ceiling at all; it is the initial backfill that needs pacing, so chunk that by date range and by object type. If you later add a warehouse target, the same discipline applies to Square to Snowflake.

How do you keep a Square Postgres sync from creating duplicates?

Make the load idempotent. Give every table a primary key on the Square id, land each page in a staging table, and upsert with INSERT ... ON CONFLICT (id) DO UPDATE SET .... Because the Square id is stable, replaying an overlapping updated-at window or retrying a failed batch updates the existing row in place rather than inserting a second copy, so the target converges to the state a clean run would produce.

The load has a consistent shape. Land each page in staging, then upsert from staging into the target in one statement so inserts and updates happen together. Child objects need the same discipline one level down: when a payment gains a new partial refund, upsert on the refund.id so re-reading the same payment does not duplicate refunds it already has. The conflict target is always the provider id, never a pairing of columns you assembled yourself, because Square guarantees the id is unique and stable while your composite keys will collide the first time a seller reuses a reference note.

Refunds and disputes are the case a naive one-time export can never keep up with. A payment settles, then days later it is partially refunded or a cardholder opens a dispute, and both change the payment's status and updated_at. The updated-at watermark catches those because the timestamp moves, but a static CSV pulled last week does not. Cancellations are the trickier tail: if a record can disappear rather than change status, a watermark will never surface it, so run a periodic reconciliation that pulls the current id set over a trailing window and flags local ids Square no longer returns. Keep a loaded_at column on every table so freshness checks and drift debugging are a one-line query, and lean on the managed Square to Postgres sync when you would rather not hand-build the refund and dispute reconciliation.

How often should you sync Square to your database?

For most retail and ops teams, every fifteen to sixty minutes on the updated-at watermark is the right steady state, with a nightly wider reconciliation pass to catch cancellations and to true up payout totals. Match the interval to the decision the data drives: a live sales dashboard wants fifteen minutes, while margin and cohort reporting are fine hourly or daily. Syncing more often than anyone reads just spends request budget for nothing.

Backfilling a seller is a different job from the steady state and should be built as one. Run a full cursor walk per object type, chunk payments and orders by created_at range so each chunk is a bounded walk you can retry independently, and record which chunks completed so a failure at 80 percent resumes rather than restarts. Load into staging first, then upsert. Only after the backfill lands should you set the watermark, and set it to the start time of the backfill rather than its end so anything that changed while the backfill was running gets picked up on the first incremental pass. Metered per-row pipeline pricing interacts badly with a busy sales day, since row counts spike exactly when you least want a surprise invoice. The Adapters plan is a flat $49 a month rather than per row, so a heavy weekend does not change the bill. If you want to see the pair running against your own Square account before you wire anything up, book a walkthrough or map it yourself with the Square to Postgres connector.

Replicate Square payments into Postgres without duplicates

Cursor pagination, updated-at watermarks, nested money flattened to integer cents, ON CONFLICT upserts, and refund reconciliation. Map the pair in the browser and pay a flat price from $49 a month.

Try the live demo

No credit card required.