Going live
A prototype and a production integration differ in about a dozen ways, and every one of them is invisible until the day it is not.
Credentials
- The key is in a secret store, not in a repository, a
.envcommitted by accident, or an MCP configuration file that is tracked. - The key is read-only unless the integration genuinely writes. Two of the 9 operations write; if you use neither, you do not need a write key.
- One key per integration.
lastUsedAtthen tells you which is still in use, and revoking one does not break the others. - A rotation plan exists, even if it is four manual steps. Mint, deploy,
watch
lastUsedAton the old key stop moving, revoke. - The webhook signing secret is stored separately from the API key. They are different credentials with different blast radii.
Requests
- Retry
rate_limitedandserver_error; do not retry the rest. A 401 retried in a loop is a loop. See the retry helper. - Backoff is exponential with jitter. A fleet of workers that all retry at exactly two seconds is a fleet that stays synchronised.
-
limit=<MaxLimit />on any full scan. The default is 25, which is four times the calls. - Loop on
has_more, not on whethernext_cursoris null. They answer different questions. - A timeout on every call. Sailo is fast and the internet is not.
Webhooks
- Signatures are verified, against the raw body. See the four ways this goes wrong.
- Deduplication on
webhook-id, with a unique constraint rather than a read-then-write — deliveries can be concurrent. - The handler answers in well under 5 seconds. Store and return; do the work off the request.
- The handler is order-independent. Upsert the whole payload rather than
applying a diff, and where order genuinely matters compare the envelope
timestamp. -
test: trueis handled. It should not create a real record in production. - The registered URL is final. Redirects count as failures; they are not followed.
- An unknown
typeis ignored, not thrown on. Events are added. - An unknown field is ignored. New fields appear inside
datawithout the payload version changing — that is the deal.
Consent
- Nothing is emailed on the strength of an order. An order’s
customer.emailis an address for a receipt. -
marketingConsentAtis checked before anybody goes into a newsletter tool — in the handler, not only in the query filter. - The nightly
?consented=truereconcile exists, because there is no event for consenting later.
Money and dates
- Arithmetic is in
cents.amountis for display. -
amountis never computed ascents / 100. It is currency-aware. - Timestamps are rendered in the shop’s
timeZonefor anything a person reads. -
stock: nullis not treated as zero. Gate listings oninStock.
Observability
- Log the
sailo-versionheader on failures. It is the fastest way to tell a contract change from a bug. - Log
webhook-idon every delivery, and the outcome. When a seller asks whether an event arrived, this is the only thing that answers. - Alert on your own 2xx rate, not on Sailo’s disable email. By the time that arrives you have lost 20 events’ worth of promptness.
- Watch handler duration, not just errors. The 5-second timeout is generous for storing a row and tight for anything else.
Failure modes to test before you ship
Each of these is something that will happen and that a happy-path test will not catch.
Your endpoint is down for an hour. Sailo retries 6 times over 15 hours, then abandons. Does your system notice the gap, and can it backfill from the REST API?
The same event arrives twice. Does anything double? A row, an email, a charge?
Two events for one object arrive out of order. Does the older one overwrite the newer?
A product referenced by an order line has been deleted. productId is
null and the line keeps its own title. Does your code assume the join
resolves?
A guest checkout. customer.clientId is null. Does your CRM sync create a
subscriber from an email that never consented?
A shop whose currency is JPY. Is amount computed anywhere as
cents / 100?
The key is revoked. Does the integration fail loudly, or silently stop syncing?
Before you launch to sellers
If you are building something other people’s Sailo shops will connect to:
- Ask for read-only where you can. A seller pasting a write key into a tool that only reads is a risk you created.
- Say which events you subscribe to, and why. Sellers tick these boxes themselves.
- Handle the plan gate. A shop below the Business plan gets
forbiddenon every call. Say so in your setup UI rather than showing a generic failure. - Test against a shop with no orders. Every list is empty, every optional object is null, and that is the state most new sellers are in.