treez
Treez API v1

Plant trees from your checkout.

One HTTPS call per order. Signed webhooks when trees go into the ground. Plug-ins for the platforms you already use.

Overview#

Treez gives your customers points for their purchases. Every 100 points plants a real, GPS-verified native tree. There are two ways to send us orders — pick whichever fits your store:

Base URL: https://api.treezplus.com/v1. JSON in and out, camelCase fields, ISO-8601 UTC timestamps. Lists use ?limit=&cursor= and return { items, nextCursor }.

Points are pending first
Order points wait for a hold period (default 14 days) before they reach the customer's wallet. Refunds and cancellations inside that window reverse them and cost you nothing.

Authentication#

Create API keys in the store portal under Developers → API keys. Send the key as a bearer token from your server:

HTTP
Authorization: Bearer sk_live_4f8a…
KeyWhat it does
sk_test_…Orders are recorded with livemode: false and fire webhooks, but never touch your budget or customers' wallets. Available while your store is pending approval.
sk_live_…Real orders and real points. Requires an approved store.
Keep keys secret
Never put a key in a browser, mobile app or public repo. We only store a hash, so a lost key can't be shown again — revoke it and create a new one. Rate limit: 600 requests per minute per key (429 RATE_LIMITED with Retry-After).

Order API#

Send an order when it's paid (or completed — your choice in Settings → Earning rules). Sending the same externalOrderId again updates the order instead of creating a duplicate.

POST/v1/orders
curl
curl -X POST https://api.treezplus.com/v1/orders \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: order-1001" \
  -H "Content-Type: application/json" \
  -d '{
    "externalOrderId": "1001",
    "amount": 12.5,
    "currency": "OMR",
    "status": "paid",
    "customer": { "email": "salim@example.com", "name": "Salim", "phone": "+96890000001" },
    "occurredAt": "2026-10-24T09:30:00Z"
  }'

Request fields

externalOrderId
string
Your order number. Unique per store.
amount
number
Order total in major units (12.5 = 12.500 OMR).
currency
string
OMR · SAR · AED · KWD · QAR · BHD · USD
status
"paid" | "completed"
Current order status.
customer
object
email (always send it — the only field used to find the Treez member), name, phone (optional, kept for your records, not used for matching). See Customer matching.
occurredAt
ISO date, optional
When the order was placed. Defaults to now.

Response

200 OK
{
  "id": "6f1c2a7e-0b7d-4c55-9a43-3f0d2a9b1e10",
  "source": "api",
  "externalOrderId": "1001",
  "livemode": true,
  "amount": 12.5,
  "currency": "OMR",
  "status": "paid",
  "customer": {
    "email": "salim@example.com",
    "phone": "+96890000001",
    "name": "Salim",
    "trees": { "matched": true, "firstName": "Salim" }
  },
  "points": 125,
  "pointsStatus": "pending",
  "skipReason": null,
  "confirmAt": "2026-11-07T09:30:00.000Z",
  "confirmedAt": null,
  "reversedAt": null,
  "createdAt": "2026-10-24T09:30:00.000Z",
  "updatedAt": "2026-10-24T09:30:00.000Z"
}

Points = floor(amount × pointsPerUnit) for the order's currency, capped by your per-order maximum. No rule for the currency or below your minimum → points: 0 with skipReason.

Lifecycle

  1. pending

    Points reserved from your budget; confirmAt = order time + hold days.

  2. confirmed

    Hold period over and the customer is on Treez → points credited.

  3. reversed

    Cancelled or refunded before confirmation → budget released.

Customer not on Treez yet? Points wait for up to 90 days and attach automatically when they sign in to the Treez app with that email.

Update the status

POST/v1/orders/:externalOrderId/status
curl
curl -X POST https://api.treezplus.com/v1/orders/1001/status \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "status": "refunded" }'

Statuses: completed, cancelled, refunded. A refund after confirmation is recorded, but points are not taken back.

List & fetch

GET/v1/orders?pointsStatus&source&limit&cursor
GET/v1/orders/:id
curl
curl "https://api.treezplus.com/v1/orders?pointsStatus=pending&limit=20" \
  -H "Authorization: Bearer sk_live_…"

Customer matching#

Points go to the Treez account whose email matches the order's customer.email. Customers and planters sign in to the Treez app with a one-time code sent to their email, so the email is the identity we trust. Always send customer.email.

customer.email
string · required for points
Matched case-insensitively (trimmed and lower-cased) against customer and planter accounts.
customer.phone
string · optional
Stored and shown to you in the portal. Never used for matching — phone numbers on Treez aren't verified.
customer.name
string · optional
Shown in your portal.
No email, no points
An order without customer.email is recorded with pointsStatus: "skipped" and skipReason: "NO_CUSTOMER_CONTACT" — those points could never be claimed, so nothing is reserved from your budget. Sending only a phone number counts as no email.

Not on Treez yet

If no account has that email, the response shows trees.matched: false and the points wait as unclaimed for up to 90 days. They attach the moment someone signs in to the Treez app with that email (and confirm right away if the hold period is over). Otherwise they expire as unclaimed_expired and go back to your budget. Test-mode orders are never matched to real customers.

Skip reasons

skipReasonMeaning
NO_CUSTOMER_CONTACTNo customer email on the order, so there's no one to give points to.
NO_RULE_FOR_CURRENCYNo earning rule for this currency.
BELOW_MINIMUMBelow your minimum order amount.
ZERO_POINTSThe order amount was too small to earn a whole point.
INSUFFICIENT_BUDGETNot enough points budget when the order arrived.
AWAITING_STATUSWaiting for the order to be completed (your earning rule awards on completion).
CANCELLED_BEFORE_AWARDCancelled before it earned points.
MERCHANT_NOT_ACTIVEYour store wasn't approved yet when the order arrived.

Idempotency#

Networks fail. Send an Idempotency-Key header (for example order-1001) and retry safely: the same key returns the original response with Idempotent-Replayed: true, and never creates points twice. Reusing a key with a different body returns 409 IDEMPOTENCY_KEY_REUSED.

Even without the header, orders are upserted by (store, source, externalOrderId).

Errors#

Errors use the HTTP status and a stable code:

4xx
{
  "error": {
    "code": "MERCHANT_NOT_ACTIVE",
    "message": "Your store is not approved yet",
    "details": {}
  }
}
CodeHTTPMeaning
VALIDATION_ERROR400A field is missing or invalid — see details.issues.
INVALID_API_KEY401The key is wrong, revoked or missing.
UNAUTHORIZED401No credentials were sent.
MERCHANT_NOT_ACTIVE403Your store isn't approved yet (live keys and real points).
TEST_MODE_UNSUPPORTED403A test key called an endpoint that only works live.
ORDER_NOT_FOUND404No order with that id for your store.
IDEMPOTENCY_KEY_REUSED409Same Idempotency-Key, different request body.
INSUFFICIENT_BUDGET409Not enough points budget (the order is kept as skipped).
RATE_LIMITED429Too many requests — wait for Retry-After seconds.
INTERNAL500Something broke on our side. Retry with the same Idempotency-Key.

Webhooks#

Add an endpoint in Developers → Webhooks and Treez will POST JSON events to it — from orders to a tree going into the ground. Every event uses the same envelope:

POST https://your-server/treez/webhooks
{
  "id": "evt_01JB2Q8M4K7Y3ZP6",
  "type": "tree.planted",
  "createdAt": "2026-10-24T09:30:00.000Z",
  "livemode": true,
  "merchantId": "b2c1…",
  "data": {
    "tree": { "id": "…", "status": "planted", "species": { "nameEn": "Ghaf" }, "…": "…" },
    "passportUrl": "https://treez.om/t/ghaf-7k4p",
    "fundingShare": 0.35
  }
}

Delivery & retries

  • Answer with any 2xx within 10 seconds. Do slow work in a background job.
  • Failed deliveries are retried after 1m · 5m · 30m · 2h · 6h · 12h · 24h (7 attempts).
  • An endpoint that fails every delivery for 3 days is disabled automatically. Re-enable it from the portal.
  • Events can arrive more than once or out of order — de-duplicate on id.

Event types

EventWhen
order.points_pendingAn order earned points that are now on hold.
order.points_confirmedThe hold period ended and the points were credited.
order.points_reversedAn order was refunded or cancelled before confirmation.
order.points_skippedAn order earned no points (no customer email, no rule, minimum, or budget).
order.refunded_after_confirmationAn order was refunded after its points were confirmed.
tree.readyA tree your customers funded is ready to plant.
tree.scheduledThat tree has a planting date.
tree.plantedIt's in the ground, with photo and GPS.
tree.verifiedA reviewer verified the planting evidence.
claim_code.claimedA customer claimed one of your QR codes.
award.createdYou gave a customer points at the till.
merchant.approvedTreez approved your store.
budget.lowYour points budget is running low.
pingA test event you send from the portal.

order.* events carry { order }; tree.* events carry { tree, passportUrl, fundingShare } and only go to stores that funded that tree.

Verify signatures#

Every request carries a signature header:

HTTP
Treez-Signature: t=1761298200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  1. Split the header on ,; read t (unix seconds) and every v1.
  2. Compute HMAC-SHA256(secret, t + "." + rawBody) as hex.
  3. Accept if it equals any v1 (constant-time compare) and t is less than 5 minutes old.
Rotating secrets
After you rotate a secret we send both signatures for 24 hours (v1=new,v1=old), so you can deploy the new secret without dropping events.
Node.js
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TREEZ_WEBHOOK_SECRET; // "whsec_…"

// Keep the raw body: the signature covers the exact bytes we sent.
app.post("/treez/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Treez-Signature") ?? "";
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);

  // Reject old timestamps (replay protection).
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.status(400).send("stale");

  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.`).update(req.body).digest("hex");
  const valid = signatures.some(
    (s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
  );
  if (!valid) return res.status(400).send("bad signature");

  const event = JSON.parse(req.body.toString("utf8"));
  switch (event.type) {
    case "order.points_confirmed":
      // event.data.order.points were credited to the customer
      break;
    case "tree.planted":
      // event.data.passportUrl — share it with your customer!
      break;
  }
  res.sendStatus(200); // answer fast; do slow work in a queue
});

Platforms#

Each platform is an adapter that turns its install flow and webhooks into the same normalised order — so points, hold periods, refunds and your webhooks work exactly the same everywhere.

Salla
Install the Treez app from the Salla App Store, then enter the link code shown in your Treez portal.
Zid
Press Connect in the portal and approve Treez on Zid (OAuth). Orders sync automatically.
WooCommerce
WooCommerce → Settings → Advanced → Webhooks → Add webhook. Topic: Order updated. Paste the delivery URL and secret from the portal.
Shopify
Enter your myshopify.com domain and approve the Treez app (OAuth).
Other website
Anything else — your own site, app or POS: use the Order API above.

Platform tokens are stored encrypted. Uninstalling the app on the platform disconnects it in Treez. Want another platform? Adding one is a single adapter on our side — tell us at hello@treez.om.

Test mode#

  • Use a sk_test_ key: orders are stored with livemode: false, appear in the portal with a TEST badge and fire webhooks — no budget or wallets change.
  • Press Send test ping on an endpoint to receive a signed ping event and see the response in the delivery log.
  • Test keys can't create claim codes or awards (TEST_MODE_UNSUPPORTED).
  • Locally, tunnel your endpoint (e.g. with ngrok) — production endpoints must use HTTPS.
Ready to plant?

Sign up, grab a test key and send your first order in five minutes.

Sign up your store