Getting a Pilebase order into the software you already run, without either of us installing anything on the other's machines.
When an order is placed at your yard on Pilebase, we POST it to a web address you give us, signed so your side can prove it came from us. If you would rather ask than be told, the same orders are available from a read API your side can poll. That is the whole surface.
There is no vendor plugin here, and that is deliberate. Across the yards we have researched in fifteen states, almost none run software with a public API we could write a plugin against, so a plugin for any one product would serve almost nobody. What every one of those systems can do, or can be made to do by whoever maintains it, is receive an HTTP request. So we built the thing a plugin sits on top of: your developer, your dispatch vendor, or a bridge tool can consume this without asking us for anything.
What the connector does: it sends your orders out.
If your system sits behind a firewall and cannot receive a request from outside, skip the address entirely and issue a key for the read API instead.
Two credentials, both issued by you from your portal, both scoped to your yard alone. There is no shared platform key and nothing here can read another yard's orders.
| Credential | Direction | What it is for |
|---|---|---|
pbk_... plus pbs_... | You call us | The read API. Sent as a bearer token. |
pbwh_... | We call you | The signing secret we sign webhooks with, so your side can verify them. |
Send the key on every read API request as one bearer token, with the key id, a dot, and the secret:
Authorization: Bearer pbk_9f3c1a....pbs_2b81f0...
Each value is shown once, at the moment it is issued. We store a hash of the API secret and cannot show it to you again, and we will not send one by email if it is lost. Revoke the key and issue another; it takes a few seconds and revoking takes effect immediately, on the very next request.
A key that reads is all a key does. There is no write path in this API at all: a credential cannot confirm an order, cancel one, change a price, or touch anything about your listing. If one leaks, what leaks is a read of your own order book, and you can kill it from your portal without calling anybody.
| Event | When it fires |
|---|---|
order.created | A buyer placed an order at your yard. |
order.confirmed | The order was confirmed, whether you tapped confirm in the order email, in the portal, or an auto-accept rule of yours did it. An auto-accepted order fires both events, in that order. |
order.cancelled | The order came off your board: you declined it, it was cancelled, or nobody answered in time and it moved to another yard. |
connector.test | You pressed the test button in your portal. Never a real order. |
The order object always carries its real status, so declined and canceled are distinguishable if your side cares. One case is worth naming: when an order times out at your yard and moves to another one, you receive order.cancelled, and the order object in it reads canceled, because at your yard that is what it is.
Every delivery is a POST with a JSON body in this envelope. data is the order.
POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Pilebase-Event: order.created
X-Pilebase-Event-Id: evt_5f2c9d41a0be77c3
X-Pilebase-Attempt: 1
X-Pilebase-Signature: t=1756070400,v1=6f1c...
{
"id": "evt_5f2c9d41a0be77c3",
"type": "order.created",
"created_at": "2026-08-24T15:31:02.117Z",
"yard_slug": "your-yard",
"data": {
"yard_slug": "your-yard",
"id": 214,
"yard_name": "Your Yard",
"status": "placed",
"created_at": "2026-08-24T15:31:02.104Z",
"fulfillment": "delivery",
"items": [
{ "name": "Premium compost", "quantity": 6, "unit": "yard",
"unit_price": 42, "line_total": 252 }
],
"subtotal": 252,
"total": 337,
"delivery": { "address": "1120 Sage Court, Parker CO 80134",
"requested_date": "2026-08-27" },
"delivery_fee": 85,
"fee_status": "priced",
"auto_confirmed": false,
"buyer_first_name": "Dana",
"buyer_phone": "+13035550143",
"pilebase_fee": { "amount": 6.74, "rate_label": "2%" }
}
}
Absent is not a value. A field the order does not carry is not in the payload at all, rather than present and null or zero. A pickup order has no delivery key. A delivery whose fee nobody has set yet has no delivery_fee key, and its fee_status reads pending. If you see a number, somebody set it; if you do not see the field, nobody has. Write your parser to expect that and you will never read a zero we invented.
No price in here was calculated by this API. Every figure is read off the stored order, which got it from your own price list and your own delivery rate card. The webhook reports what was decided; it does not decide anything, and nothing your side sends back changes a number.
The buyer's details are the ones loading a truck needs and no more: a first name and a phone number, plus the delivery address on a delivery. Not their email, not their account, not their other orders.
Anybody can POST JSON at a public URL, so the signature is the whole of the authentication. Check it before you act on the body.
X-Pilebase-Signature looks like t=1756070400,v1=6f1c.... The v1 value is HMAC-SHA256 over the exact bytes of t, a full stop, and the raw request body, keyed with your signing secret, in lower-case hex. Several v1 values can appear at once during a rotation; accept the request if any of them matches.
Verify against the raw bytes. Parsing the JSON and re-serializing it does not give you the same bytes back: key order, number formatting and whitespace all move. Keep the raw body.
Compare in constant time, and reject a timestamp more than five minutes from now in either direction. That second check is what stops somebody replaying a copy of a real request they captured.
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = String(header).split(',').map((p) => p.trim());
const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2));
const given = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
if (!Number.isFinite(t) || !given.length) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // replay window
const expected = crypto.createHmac('sha256', secret)
.update(t + '.' + rawBody).digest('hex');
const want = Buffer.from(expected, 'utf8');
return given.some((g) => {
const got = Buffer.from(g, 'utf8');
// Length first: timingSafeEqual throws on a mismatch, and a verifier
// that throws is a check that fails open.
return got.length === want.length && crypto.timingSafeEqual(got, want);
});
}
Answer 2xx when you have taken the event. Anything else, or no answer at all, is a failure and we will try again. Answer quickly: take the event, then do your work.
Rotating the secret is one click in your portal. The old one stops working the moment the new one is issued, so update your receiver in the same sitting.
We try up to six times: immediately, then after about a minute, five minutes, fifteen minutes, an hour, and three hours. A delivery that never lands is marked failed and shows in the log with the reason.
Every retry carries the same X-Pilebase-Event-Id. Store that id and ignore an event you have already applied. That is the whole contract: a duplicated order at your yard is worse than a slow one, so we resend rather than give up, and you dedupe rather than trust the network.
We also do not queue the same event twice on our side, so a repeat you receive is a retry of one delivery and never a second order.
If an endpoint fails twenty deliveries in a row we switch it off and say so in your portal, so a server that has been decommissioned does not collect requests forever. Turning it back on is one click.
For a system that cannot receive a request from outside, or an integrator who would simply rather poll. Same orders, same payload shape as data above, same credential, same scoping.
GET https://pilebase.io/api/marketplace/connector/v1/whoami
GET https://pilebase.io/api/marketplace/connector/v1/orders
GET https://pilebase.io/api/marketplace/connector/v1/orders/:id
/orders takes three optional query parameters: status (one or several, comma separated, matching the order status), since (an ISO 8601 timestamp), and limit (up to 200, default 100). It answers newest first:
{ "yard_slug": "your-yard", "count": 2, "total": 2, "orders": [ ... ] }
An order that is not yours is a 404, not a 403: another yard's order is not something your credential is told exists. A credential that is not usable is a 401 with one sentence, whatever was actually wrong with it. Poll politely: the budget is 600 requests an hour per key, which is a call every six seconds, and a 429 means slow down.
A short list is more useful than a long one, so here is everything this does not do, rather than a hint that it might.
Costs nothing. The connector is part of a claimed listing, and a claimed listing is free. The only fee on an order the marketplace brings you is 2% of the order, or 2.5% if you run Complete on an order the marketplace brings you.
Tell us what you run and what you want it to do. If your case needs something this does not cover yet, we would rather hear it than guess.
Talk to us