Developers

Protected payments in one API call.

Mint a branded, hosted checkout link — escrow-held or pay-now — from any marketplace or app. Track it as a transaction, forward funds to the service pro, and stay in sync with signed webhooks. Money-moving endpoints stay locked behind LEGAL_REVIEW so you can build and test the entire flow before a single real dollar moves.

QuickstartUse casesAuthenticationConventions & errorsPayment linksTransactionsSubscriptionsEscrow lifecyclePayouts & payeesDisputesEmbed widgetWebhooksGo-live checklist

Quickstart — integrate in 5 steps

The fastest path to going live. Each step links to the full reference below.

  1. Get your workspace & API key. Request access — we provision a workspace for your domain and issue a key (se_live_… / se_test_…). Each key is scoped to your workspace.
  2. Create a payment link. One POST returns a branded URL you redirect (or hand) your customer to. Choose escrow (held until approved) or direct (pay-now).
  3. Pass a returnUrl. After paying, we show a branded confirmation and send the customer right back to the page they came from on your site.
  4. Listen for webhooks. We POST signed events on milestone state changes and when a direct payment settles (payment.paid) so your system and ours never drift — even if the buyer closes the tab.
  5. Release & forward. On approval, release the milestone and forward the funds to the service pro (recorded; paid off-platform).
curl — your first payment link
curl -X POST https://www.serviceescrow.com/api/payments/links \
  -H "Authorization: Bearer se_live_<YOUR_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "direct",
    "amountCents": 12500,
    "description": "Gutter cleaning — deposit",
    "returnUrl": "https://handyman.com/orders/8821",
    "cancelUrl": "https://handyman.com/orders/8821?checkout=cancelled"
  }'

# → 200
# {
#   "ok": true,
#   "mode": "direct",
#   "url": "https://www.serviceescrow.com/pay/se_pl_9f3c…",  ← ALWAYS redirect the customer here (branded)
#   "token": "se_pl_9f3c…",
#   "reference": "clx…",         ← your transaction id
#   "stripeUrl": "https://checkout.stripe.com/…",  ← raw Stripe; bypasses branding (compat only), null until LEGAL_REVIEW
#   "stripeReady": true,
#   "expiresAt": null
# }
node — the same call
const res = await fetch("https://www.serviceescrow.com/api/payments/links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SERVICEESCROW_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    mode: "direct",
    amountCents: 12500,
    description: "Gutter cleaning — deposit",
    returnUrl: "https://handyman.com/orders/8821",
    cancelUrl: "https://handyman.com/orders/8821?checkout=cancelled",
  }),
});
const { url } = await res.json();
// redirect the buyer to `url` (the branded /pay/<token> page)

Use cases

End-to-end recipes that chain the endpoints below into the flows most integrations need. Set BASE=https://www.serviceescrow.com and your key, then run them top to bottom.

Escrow-protected project with milestones

Home-services marketplace

A homeowner books a multi-milestone job. Funds are held per milestone and only released to the pro once each stage is approved.

1 · mirror the project → 2 · fund → 3 · release → 4 · pay the pro
# 1) Mirror the project & its milestones into escrow
curl -X POST $BASE/api/escrow/plans \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "externalProjectId": "PROJ-123", "foundingMember": true }'

# 2) Mint a branded hosted escrow link for milestone M-1 (share the returned url)
curl -X POST $BASE/api/payments/links \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "mode": "escrow", "externalMilestoneId": "M-1",
        "returnUrl": "https://handyman.com/orders/123" }'

#    Homeowner pays → Stripe captures & holds → you receive a signed milestone.funded webhook

# 3) Homeowner approves the stage → release (fee kept, pro marked owed)
curl -X POST $BASE/api/escrow/milestones/M1_ID/release \
  -H "Authorization: Bearer se_live_<KEY>"

# 4) Record the off-platform payout to the pro
curl -X POST $BASE/api/escrow/milestones/M1_ID/forward \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "receiverName": "Jane'"'"'s Plumbing", "method": "ach", "reference": "ACH-889912" }'

One-off pay-now link

Any site collecting a deposit

Charge a customer once (deposit, invoice, or booking fee) with a branded checkout page — no milestone lifecycle.

create → share → track → forward
# Create a direct payment link
curl -X POST $BASE/api/payments/links \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "mode": "direct", "amountCents": 12500, "description": "Gutter cleaning deposit",
        "returnUrl": "https://site.com/orders/8821",
        "cancelUrl": "https://site.com/orders/8821?cancelled=1" }'
# → share the returned "url" — the customer pays on the co-branded /pay/<token> page

# Track it any time
curl "$BASE/api/transactions?status=paid&mode=direct" \
  -H "Authorization: Bearer se_live_<KEY>"

# Forward the collected funds to the pro (auto-resolves a saved payee)
curl -X POST $BASE/api/transactions/TX_ID/forward \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "contractorRef": "pro_5521", "reference": "CHK-1042" }'

Yearly subscription with per-cycle payouts

Recurring memberships / retainers

Bill a customer yearly through Stripe. Each paid cycle becomes a charge owed to the pro until you forward it.

create → list cycles → forward each → cancel
# Create a recurring (yearly) link
curl -X POST $BASE/api/payments/links \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "mode": "direct", "interval": "year", "amountCents": 19900,
        "description": "Annual maintenance plan" }'
#    Customer subscribes on the hosted page → each renewal fires invoice.paid → a recurring charge (owed)

# List cycles owed to the pro
curl "$BASE/api/recurring-charges?payoutStatus=owed" \
  -H "Authorization: Bearer se_live_<KEY>"

# Forward one paid cycle
curl -X POST $BASE/api/recurring-charges/CHARGE_ID/forward \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "contractorRef": "pro_5521", "reference": "ACH-2027-01" }'

# Cancel when the customer churns (no further cycles billed)
curl -X DELETE $BASE/api/transactions/TX_ID/subscription \
  -H "Authorization: Bearer se_live_<KEY>"

Save contractor payout profiles once

Paying the same pros repeatedly

Store each pro's payout destination once, keyed by your contractorRef, so forwards auto-resolve without re-sending receiver details.

save once → forward without receiver fields
# Save the payout profile
curl -X POST $BASE/api/payees \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "contractorRef": "pro_5521", "name": "Jane'"'"'s Plumbing",
        "method": "ach", "detail": "ACH …4821" }'

# Forward without receiver fields — the profile fills them in
curl -X POST $BASE/api/transactions/TX_ID/forward \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "contractorRef": "pro_5521", "reference": "ACH-889912" }'

# Archive a profile when a pro leaves
curl -X PATCH $BASE/api/payees/PAYEE_ID \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "status": "archived" }'

Dispute and refund a milestone

Support / trust & safety

A homeowner reports incomplete work. Open a dispute (funds stay held), resolve it, and refund if warranted.

open dispute → resolve → refund
# Open a dispute — the milestone moves to 'disputed', funds stay held
curl -X POST $BASE/api/disputes \
  -H "Authorization: Bearer se_live_<KEY>" \
  -d '{ "milestoneId": "M1_ID", "reason": "Work incomplete", "openedBy": "homeowner" }'

# Resolve it (handled by the ServiceEscrow ops team in the admin console)
# PATCH /api/disputes { "id": "DISPUTE_ID", "status": "resolved", "resolutionNote": "Refunded buyer" }

# Refund the buyer's captured charge
curl -X POST $BASE/api/escrow/milestones/M1_ID/refund \
  -H "Authorization: Bearer se_live_<KEY>"

Authentication

Every /api endpoint (except the inbound Stripe webhook) requires your workspace bearer key. Send it on every request:

Authorization: Bearer se_live_<YOUR_KEY>
  • Workspace-scoped. A key resolves to exactly one workspace (tenant). It can never read or mutate another workspace's data.
  • Live vs test. se_live_… and se_test_… keys are issued per workspace. Keep your keys server-side — never ship them to a browser.
  • Rotate anytime. Revoked keys immediately return 401. A suspended workspace returns 403.
  • Admin-only endpoints (marked below) are driven by the ServiceEscrow admin console session, not a bearer key — they're for your ops team, not your integration.

Base URL, conventions & errors

base url
https://www.serviceescrow.com

All requests and responses are JSON. Successful responses include "ok": true. Errors return { "ok": false, "error": "message" } with an appropriate status:

status codes
200 / 201  success
400        validation error (bad or missing fields)
401        missing / invalid / revoked API key
403        workspace suspended
404        resource not found (or not in your workspace)
409        conflict — wrong state (e.g. milestone already funded, tx already paid)
423        Locked — money-moving endpoint disabled until LEGAL_REVIEW=true

The LEGAL_REVIEW gate. Fund/release/refund and raw Stripe URLs stay behind 423 Locked until legal + payment review is complete for your market. Payment links and transactions can still be created and tested — the hosted page simply mints the Stripe session once the gate is open.

Idempotency. Creating an escrow plan for an externalProjectId that already exists returns the existing plan with "deduped": true instead of creating a duplicate.

One call mints a branded, hosted ServiceEscrow page at /pay/<token>. The payer sees a reassurance page co-branded with your logo, then continues to Stripe Checkout. Share url — it never carries a stale Stripe link.

POST/api/payments/linksworkspace key
escrow — fund an existing milestone
{
  "mode": "escrow",
  "externalMilestoneId": "M-1",
  "returnUrl": "https://handyman.com/orders/123",
  "cancelUrl": "https://handyman.com/cart"
}
direct — one-off pay-now (no hold, no milestone)
{
  "mode": "direct",
  "amountCents": 5000,
  "description": "Deposit",
  "currency": "usd",              // optional, defaults to usd
  "expiresInDays": 7,             // optional
  "metadata": { "orderId": "8821" }, // optional (string → string)
  "returnUrl": "https://handyman.com/orders/8821",
  "cancelUrl": "https://handyman.com/orders/8821?cancelled=1"
}
response
{
  "ok": true,
  "mode": "direct",
  "url": "https://www.serviceescrow.com/pay/se_pl_…",  // SHARE THIS
  "token": "se_pl_…",
  "reference": "clx…",           // the transaction id
  "stripeUrl": "https://checkout.stripe.com/…",  // raw Stripe; bypasses branding (compat only), null until LEGAL_REVIEW
  "stripeReady": true,
  "expiresAt": "2026-07-15T00:00:00.000Z"
}

Branding & sending the payer back

The hosted page shows the ServiceEscrow logo co-branded with your marketplace logo so payers recognise where they are. After a successful payment we show a branded confirmation, then automatically return the payer to your returnUrl:

  • returnUrl — where the payer lands after paying. Deep-link it to the exact order or booking so the round-trip feels seamless. Omit it to leave them on our confirmation page.
  • cancelUrl — where they go if they back out. No charge is made.
  • Both must be absolute https:// URLs (validated; no open redirects).
the post-payment round-trip
buyer on handyman.com
   → /api/payments/links (you)          → returns branded url
   → https://serviceescrow.com/pay/<token>  (co-branded reassurance page)
   → Stripe Checkout                    (card entry, PCI handled by Stripe)
   → branded "Payment received" confirmation
   → back to your returnUrl             (e.g. handyman.com/orders/8821)

Transactions

A transaction is a payment link — the object ServiceEscrow owns for both direct and escrow payments. The Transactions API gives you search, filtering, and safe CRUD, always scoped to your workspace.

GET/api/transactionsworkspace key
list, search & filter
GET /api/transactions?q=deposit&mode=direct&status=paid&from=2026-01-01&to=2026-12-31&limit=25&offset=0

# q       free text over description / token / id
# mode    direct | escrow
# status  active | paid | void | expired
# from/to ISO dates on createdAt
# limit   1–100 (default 25),  offset for pagination

# → {
#   "ok": true, "total": 42, "limit": 25, "offset": 0,
#   "transactions": [
#     { "id": "clx…", "token": "se_pl_…", "url": "https://…/pay/se_pl_…",
#       "mode": "direct", "amountCents": 5000, "currency": "usd",
#       "description": "Deposit", "status": "paid", "metadata": {…},
#       "returnUrl": "…", "expiresAt": null, "paidAt": "…", "createdAt": "…" }
#   ]
# }
POST/api/transactionsworkspace key
create (returns the tx + hosted url)
{ "mode": "direct", "amountCents": 5000, "description": "Deposit", "expiresInDays": 7 }
{ "mode": "escrow", "externalMilestoneId": "M-1" }

# → 201 { "ok": true, "transaction": { … }, "stripeUrl": "…", "stripeReady": true }
GET/api/transactions/:idworkspace key
PATCH/api/transactions/:idworkspace key
DELETE/api/transactions/:idworkspace key
update / void / delete
PATCH /api/transactions/:id
{ "description": "Updated note", "metadata": { "po": "778" }, "expiresInDays": 14 }
{ "status": "void" }     // cancel an unpaid link

DELETE /api/transactions/:id   // unpaid only

# Safe by design: amount and mode are immutable. A PAID transaction can only have its
# description/metadata edited — it can never be voided or deleted (kept for your records).

Recurring subscriptions

Turn a direct payment link into a yearly subscription (e.g. an annual maintenance plan) by adding interval: "year". The customer subscribes on the same branded hosted page, and Stripe bills them automatically each year. Escrow links can't be recurring (there's no per-cycle "approve" step).

POST/api/payments/linksworkspace key
create a yearly subscription link
{
  "mode": "direct",
  "interval": "year",              // makes it recurring; amountCents is the per-year price
  "amountCents": 19900,
  "description": "Annual maintenance plan",
  "returnUrl": "https://handyman.com/account/plans"
}

# → { "ok": true, "mode": "direct", "recurring": true, "interval": "year",
#     "url": "https://www.serviceescrow.com/pay/se_pl_…", "token": "se_pl_…" }

Each paid year is recorded as a recurring charge that's owed to the service pro — forward it exactly like a one-off, per cycle:

GET/api/recurring-charges?paymentLinkId=&payoutStatus=owedworkspace key
POST/api/recurring-charges/:id/forwardworkspace key
list cycles & pay the pro for one
GET /api/recurring-charges?payoutStatus=owed
# → { "ok": true, "charges": [ { "id": "…", "amountCents": 19900, "payoutStatus": "owed",
#      "periodStart": "…", "periodEnd": "…", "paidAt": "…" } ] }

POST /api/recurring-charges/:id/forward
{ "receiverName": "Jane's Plumbing", "method": "ach", "reference": "ACH-2027-01" }
# or { "contractorRef": "pro_5521", "reference": "ACH-2027-01" }  ← auto-resolve a saved payee
DELETE/api/transactions/:id/subscriptionworkspace key
cancel a subscription
DELETE /api/transactions/:id/subscription
# → { "ok": true, "subscriptionStatus": "canceled" }   (no further cycles are billed)

Subscription status (active / past_due / canceled) is kept in sync from Stripe automatically, and each transaction reports recurring, interval, and subscriptionStatus.

Escrow lifecycle

Capture-only model: the buyer's payment is captured into the platform balance and held. On release we record the platform fee and the amount owed to the pro, which is then forwarded off-platform. There is no Stripe Connect transfer.

milestone states
pending → held (funded) → released (payoutStatus: owed) → paid_out
                     ↘ refunded          ↘ disputed

1 · Mirror a project & its milestones

POST/api/escrow/plansworkspace key
{ "externalProjectId": "PROJ-123", "foundingMember": true }

# → { "ok": true, "plan": { "id": "…", "milestones": [ { "id": "…", "externalMilestoneId": "M-1",
#      "amountCents": 150000, "status": "pending" } ] }, "deduped": false }

2 · Fund a milestone (buyer pays → captured & held)

POST/api/escrow/milestones/:id/fundworkspace key · gated
{ "returnUrl"?: "…", "cancelUrl"?: "…" }   // optional
# → { "ok": true,
#     "url": "https://www.serviceescrow.com/pay/se_pl_…",   ← branded page — SHARE THIS
#     "token": "se_pl_…",
#     "checkout_url": "https://checkout.stripe.com/…" }      ← raw Stripe (bypasses branding; compat only)
# Funding now goes through the same branded /pay page as direct payments.
# (equivalent to /api/payments/links with mode:"escrow")

3 · Release on approval (fee kept, pro owed)

POST/api/escrow/milestones/:id/releaseworkspace key · gated
# fee = 2.9% (1.9% founding members), capped $290 per project
# → { "ok": true, "status": "released", "owed_to_pro": 145650, "fee": 4350, "payout_status": "owed" }

4 · Refund the buyer

POST/api/escrow/milestones/:id/refundworkspace key · gated
# refunds the buyer's captured charge via Stripe. If the pro was already paid out:
# → { "ok": true, "refunded": 150000, "refund_id": "re_…", "manual_clawback_needed": true }

Payouts & payout profiles

There are two parties: the payer (homeowner / buyer) who funds, and the receiver (service doer) who gets paid. Forwarding records the payout (who + method + reference) — you pay the pro off-platform (ACH / check / PayPal) and we keep the ledger straight.

Forward a released milestone

POST/api/escrow/milestones/:id/forwardworkspace key
{ "receiverName": "Jane's Plumbing", "method": "ach",
  "receiverEmail"?: "jane@example.com",
  "detail"?: "acct …4821",        // SAFE reference only — full account numbers are rejected
  "reference"?: "ACH-889912" }

# → { "ok": true, "status": "released", "payout_status": "paid_out",
#     "paid": 145650, "receiver": "Jane's Plumbing", "method": "ach", "payout_ref": "ACH-889912" }

Forward a paid direct transaction

POST/api/transactions/:id/forwardworkspace key
{ "receiverName": "Jane's Plumbing", "method": "check", "reference": "CHK-1042" }
# → { "ok": true, "transaction": { … "status": "paid" } }
# method: ach | check | paypal | stripe | other

Payout profiles (save once, auto-resolve)

Store a contractor's payout destination once — keyed by contractorRef — and a forward auto-resolves it, so you don't re-send receiver details every time. Capture-only & safe: store a method plus a reference / last-4 / note only.

GET/api/payees?q=&contractorRef=workspace key
POST/api/payeesworkspace key
PATCH/api/payees/:idworkspace key
DELETE/api/payees/:idworkspace key
POST /api/payees
{ "contractorRef": "pro_5521", "name": "Jane's Plumbing",
  "method": "ach", "email": "jane@example.com", "detail": "ACH …4821" }
# → 201 { "ok": true, "payee": { "id": "…", "contractorRef": "pro_5521", "name": "…", "method": "ach" } }

# Then forward WITHOUT receiver details — the saved profile fills them in:
POST /api/escrow/milestones/:id/forward   { "reference": "ACH-889912" }
POST /api/transactions/:id/forward        { "contractorRef": "pro_5521", "reference": "ACH-889912" }

Admin-only equivalents (used by your ops team in the console's Payouts / Payees tabs): POST /api/admin/payouts, POST /api/escrow/milestones/:id/mark-paid-out, and GET/POST/PATCH/DELETE /api/admin/payees.

Disputes

POST/api/disputesworkspace key
PATCH/api/disputesadmin only
POST /api/disputes
{ "milestoneId": "…", "reason": "Work incomplete", "openedBy"?: "homeowner" }
# → { "ok": true, "dispute": { … } }   (milestone moves to 'disputed'; funds stay held)

PATCH /api/disputes   (admin)
{ "id": "…", "status": "resolved", "resolutionNote": "Refunded buyer" }

Embed checkout widget

Start checkout from the browser with a publishable key — no secret se_live_… on the client. Mint the key under Admin → Workspace → Webhook Settings.

POST/api/embed/checkoutpublishable key
drop-in script
<script src="https://www.serviceescrow.com/embed/checkout.js"></script>
<script>
  ServiceEscrow.checkout({
    publishableKey: "se_pk_live_…",
    amountCents: 9900,
    description: "Homeowner Plus — 1 year",
    returnUrl: "https://yoursite.com/done",
    cancelUrl: "https://yoursite.com/cancel",
    metadata: { kind: "homeowner_plus", homeowner_id: "42" },
  });
</script>

# → redirects to https://www.serviceescrow.com/pay/<token>
# After pay we POST payment.paid to your webhookUrl with the same metadata.
curl (same API)
curl -X POST https://www.serviceescrow.com/api/embed/checkout \
  -H "x-se-publishable-key: se_pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "amountCents": 4900, "description": "Deep Review",
        "metadata": { "kind": "deep_review", "project_id": "123" } }'

Webhooks

Inbound — point Stripe at us

Configure a Stripe webhook to /api/stripe/webhook. Events are signature-verified and idempotent; a full payout event log keeps both systems in sync. This is our endpoint — marketplaces do not call it.

POST/api/stripe/webhookStripe signature
subscribe Stripe to
checkout.session.completed   → milestone held / transaction paid (+ payment.paid outbound)
payment_intent.succeeded     → same as above (one-time PI path)
invoice.paid                 → recurring charge recorded (owed)
invoice.payment_failed       → subscription past_due
customer.subscription.*      → subscription status synced
charge.dispute.created       → milestone disputed (+ milestone.disputed outbound)

Outbound — we call your endpoint

We POST signed JSON to your workspace webhook URL (Admin → Settings → Webhook Settings). Deliveries retry with backoff (up to 8 attempts). Verify the x-serviceescrow-signature header — an HMAC-SHA256 of the raw body with your workspace secret — before trusting the event. Respond 2xx to acknowledge.

POST<your webhookUrl>HMAC signature

Events: milestone.funded · milestone.released · milestone.refunded · milestone.disputed · payment.paid

milestone.* payload
POST <your webhook url>
x-serviceescrow-event: milestone.released
x-serviceescrow-signature: sha256=<hmac>

{
  "event": "milestone.released",
  "workspace": "handyman",
  "externalProjectId": "PROJ-123",   // your project id
  "externalMilestoneId": "M-1",      // your milestone id
  "seMilestoneId": "cms2w0mhp0002wge8hatbtb51",  // ours — the release API takes this one
  "status": "released",
  "amountCents": 150000,
  "payoutStatus": "owed",
  "ts": "2026-01-01T00:00:00.000Z"
}
payment.paid payload (mode: direct / subscription)
POST <your webhook url>
x-serviceescrow-event: payment.paid
x-serviceescrow-signature: sha256=<hmac>

{
  "event": "payment.paid",
  "workspace": "homemanager",
  "reference": "clx…",          // same id as GET /api/transactions/:id
  "token": "pay_…",
  "mode": "direct",
  "status": "paid",
  "amountCents": 9900,
  "currency": "usd",
  "description": "Homeowner Plus — 1 year",
  "metadata": { "kind": "homeowner_plus", "homeowner_id": "42" },
  "interval": null,
  "paidAt": "2026-01-01T00:00:00.000Z",
  "ts": "2026-01-01T00:00:00.000Z"
}

# Fires once when a direct (or subscription) link first settles.
# Use metadata you passed on POST /api/payments/links to activate products server-side
# (do not depend on the buyer returning to returnUrl). Escrow funding still uses milestone.funded.
node — verify the signature (Express)
import express from "express";
import crypto from "crypto";

const app = express();

app.post("/webhooks/serviceescrow",
  express.raw({ type: "application/json" }), // IMPORTANT: verify the RAW body
  (req, res) => {
    const sig = req.header("x-serviceescrow-signature") || "";
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", process.env.SE_WEBHOOK_SECRET)
        .update(req.body)      // Buffer of the raw body
        .digest("hex");

    const ok =
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.status(401).send("bad signature");

    const event = JSON.parse(req.body.toString());
    if (event.event === "payment.paid") {
      // activate product from event.reference + event.metadata
    } else {
      // e.g. mark PROJ-123 / M-1 as released in your system
    }
    res.sendStatus(200);   // respond 2xx to acknowledge (else we retry)
  });

Health & go-live checklist

GET/api/healthpublic
GET /api/health → { "ok": true, "service": "serviceescrow",
                    "db": true, "stripe": true, "legal_review": false }
  • Build & test the full flow with your se_test_… key while LEGAL_REVIEW is off.
  • Set your workspace webhook URL & secret and verify signatures for both milestone.* and payment.paid.
  • Save your contractors' payout profiles so forwards auto-resolve.
  • Deep-link every returnUrl back to the exact order/booking for a seamless round-trip.
  • Complete legal + payment review, flip LEGAL_REVIEW=true, and go live with se_live_….

Ready to build? Request early access — we'll set up your workspace and keys, and help you wire the first call.