Skip to content

Partner API ​

The partner API lets your own systems call landplan.io: real estate and lending platforms, builders, proptech companies, manufacturers and distributors. It answers with JSON. It is a paid service, metered per call.

  • Base URL: https://landplan.io/api/partner/v1
  • OpenAPI document: /api/partner/v1/openapi.json. Use it to generate a client in your language.
  • Version: v1. Changes that would break a client get a new version (/api/partner/v2). Fields can be added to v1 answers, so ignore fields you don't know.

Preliminary results

Feasibility, designs and estimates are automated and preliminary. They are not a soil evaluation, a stamped design or a permit. A licensed designer and the local health department decide.

Getting access ​

Use of the API is governed by the Partner API Terms, which you accept when you ask for access or issue a key.

  1. Write to support@landplan.io with what you'd like to build. We set up an API client for your landplan.io account, with a plan and the scopes you need.
  2. Open the account menu, then Account > API keys. Issue a test key first.
  3. When you're ready, ask us to turn on live keys, then issue a live key.

Until a client is set up, the API keys page says No API access yet.

The same keys open your maps in QGIS: give a key the wfs:edit or wfs:read scope (free, never metered) and see Edit your maps in QGIS.

A key looks like lp_1a2b3c4d_<secret>. It is shown once, when you issue it. landplan.io keeps only a fingerprint (an HMAC) of the secret, so a lost key can't be recovered, only replaced. Keep keys on your servers. Never put one in a web page or a mobile app.

Test and live keys ​

Test keyLive key
CostFree, never billedMetered against your plan
OutputReal answers marked as sample output (Meta.Sample: true)Production output
Limits10 calls a minute, 200 a dayYour plan's limits

API keys in the app ​

The API keys page (Account > API keys in the account menu) has a card for each API client of your account:

  • The client's name, its plan, whether it is Active or Suspended, and the scopes it may use.
  • This month: the units used out of the plan's included units.
  • New key, and the table of its keys.
  • Webhook: the client's webhook. See Webhooks.

Read the API docs at the top opens this page.

Issue a key ​

Click New key on the client's card. New API key opens:

FieldWhat it does
NameYour name for the key, for example Production server.
Test (free, sample output) or Live (metered)The key's mode. Live is offered once landplan.io has turned on live keys for the client: until then it says Live keys for this client are issued by landplan.io.
ScopesWhat the key may call. Empty: all of the client's scopes.
Allowed addresses (optional)Addresses or CIDR ranges the key may be used from, space separated. Empty: anywhere.
ExpiresNever, 30 days, 90 days or 1 year.
Calls / minute and Calls / dayThe key's own limits. Empty: the plan's.
Units / monthA monthly cap of units for the key. Empty: no cap.

Click Issue key. Copy your API key shows the key once. Click Copy, then I've copied it. Send it as Authorization: Bearer <key>.

The keys table ​

ColumnWhat it shows
KeyThe key's name, and a hint of the key such as its prefix
Modetest or live
Statusactive, revoked or expired, and the expiry date of an active key
ScopesThe key's scopes
LimitsIts own limits, if any: calls a minute, calls a day, units a month and allowed addresses
Last usedWhen it last made a call, or Never

Revoke stops a key at once.

Rotating a key ​

Each client can have two active keys of each mode. To rotate a key without downtime:

  1. Issue the new key.
  2. Move your systems to it.
  3. Revoke the old key. It stops working at once.

Organization keys ​

An organization on the Enterprise plan can hold its own API keys, so an integration keeps working when people join or leave. The organization's Owners and Admins manage them on the organization's API keys page. See Organization. An organization that isn't on Enterprise sees API access is part of Enterprise there, with Ask about Enterprise.

  • What a key reaches. An organization key acts as the organization, not as a person. It reaches the organization's projects (not the ones made private), and never a member's personal projects or another organization's. A view-only project, or an organization that is read-only, can be read but not used for designs.
  • Billing. Live calls are metered against the organization's API plan and invoiced to the organization. Usage this month on the page shows the units used. Test keys are free, as for every client.
  • Creating a key. Choose a name, test or live, the scopes, and optionally the addresses it may be used from, an expiry and a daily cap. The key, lp_<prefix>_<secret>, is shown once. The table then shows its name, prefix, mode, scopes, last use and who created it.
  • Rotating a key. Rotate makes a new key with the same settings and shows it once. The old key keeps working for 24 hours, then stops, so you can move your systems over without downtime. Two keys of each mode can be active at once.
  • Revoking a key. Revoke stops a key at once.
  • When keys stop working. The keys of an organization that leaves the Enterprise plan, becomes read-only or is closed are refused (401, with the reason in detail) until that changes.

Every create, rotate and revoke is written to the organization's audit log with the key's prefix. The secret is never logged.

To add API access to your organization's plan, write to support@landplan.io.

Calling the API ​

Send the key in the Authorization header (or in X-Api-Key):

bash
curl https://landplan.io/api/partner/v1/me \
  -H "Authorization: Bearer lp_1a2b3c4d_<secret>"

A browser cookie is never accepted. Signing in to landplan.io doesn't give access to the partner API.

Idempotency-Key ​

Every POST needs an Idempotency-Key header: 8 to 64 letters, digits or _ . : -. Use a new value for each call, and the same value when you retry a call. A retry with the same value is never charged twice. Its answer says Meta.Repeat: true. A retry must send the same request: a key already used for another request (another body or another endpoint) is refused with 409.

bash
curl https://landplan.io/api/partner/v1/feasibility \
  -H "Authorization: Bearer lp_1a2b3c4d_<secret>" \
  -H "Idempotency-Key: order-1042-feasibility" \
  -H "Content-Type: application/json" \
  -d '{"Latitude": 39.96, "Longitude": -82.99, "State": "OH", "County": "Franklin", "Bedrooms": 3}'

What every answer carries ​

Each answer has a Meta object:

FieldMeaning
Modelive or test
Sampletrue for a test key: sample output, not for production use
UnitsUnits this call used (0 for a test key, a refused call or a repeat)
RepeatA repeat of an Idempotency-Key that was already charged
TraceIdQuote it to support. It is also in our logs
NoticeA note about the output

The X-LandPlan-Units response header gives the units too.

Errors ​

Errors are ProblemDetails (application/problem+json) with a title, a detail and a traceId. They never contain internal error text.

StatusWhen
400Invalid input, or no Idempotency-Key on a POST
401No key, or a wrong, revoked or expired key, a suspended client, or an address outside the key's allow-list
402Your plan's monthly units, or the key's own monthly quota, are used up
403The key doesn't have the operation's scope
404The record doesn't exist, or your account can't see it
409The Idempotency-Key was already used for another request (send a new key for a new request)
429The key's calls a minute or a day are used up. Retry-After says when to try again

Scopes ​

A key can call only what its scopes allow. An admin sets the scopes a client may give its keys.

ScopeOperations
feasibility:readPOST /feasibility
design:readPOST /designs
estimate:readPOST /estimates
regulatory:readGET /regulations
products:readPOST /products/recommendations
catalog:readGET /catalog/...
market:readGET /market/price-index, GET /market/demand
quotes:readGET /quotes, GET /quotes/{id} (your company's quote requests)
quotes:writePOST /quotes/{id}/reply
catalog:writePOST /catalog/price-lists, GET /catalog/price-lists/{id} (your company's price list)
wfs:readYour maps in QGIS and other GIS programs (WFS), read only: Edit your maps in QGIS
wfs:editYour maps in QGIS and other GIS programs, read and edit

The WFS scopes can be given to any client's keys, but only when asked for (a key without scopes gets the client's partner scopes). They call no partner operation and are never metered. feasibility:read also covers POST /feasibility/bulk. GET /me (the key, its limits and this month's use) and GET /jobs/{id} need no scope and are free.

New clients get the read scopes. The company scopes (quotes:*, catalog:write) are given to a client that landplan.io has linked to your company in the catalog; catalog:write also needs the company to be verified.

Endpoints ​

Septic feasibility ​

POST /feasibility screens a site for a septic system. Send the point, the state, the county (optional) and the bedrooms.

  • Soils: read from the USDA soil survey (SSURGO) at the point, unless you send soil test data in Site (slope, water table, bedrock and restrictive layer depths, percolation rate, flooding). Your data wins over the survey. Send "UseSoilSurvey": false to skip the survey.
  • Answer: the design flow, the minimum septic tank under the area's rules, the soils used, site checks (depth to the limiting layer, slope, flooding, hydric soils, percolation), the area's system types ranked from likely to ruled out with general cost ranges, and the area's setbacks. Feasibility is likely, possible, unlikely or unknown.
  • With a project: send ProjectId (a project of your account with soil tests). The design engine's options for its soil tests are added in Engine.

The screen uses general rules, not a state's code. For example, a conventional trench usually needs about 48 inches of usable soil.

Design generation ​

POST /designs runs landplan.io's design engine on a project of your account. The project needs at least one soil test.

  • With only ProjectId: every soil test's system options, as the engine sizes them: valid or not, loading rate, absorption area, length and width, tanks and the engine's messages.
  • With TestId and SystemType too: the default design of that system on that test, with its priced product lines.

NotIncluded lists what the server can't produce yet. The layout on the parcel, the drawings and the full plan set are made in the app, and a stamped design comes from a licensed designer.

Cost estimates ​

POST /estimates estimates a system's cost in one of two ways:

  • From a design: send ProjectId, TestId and SystemType. The answer lists the design's priced lines, from your account's price books and distributor offers.
  • From an area: send State, SystemType and Bedrooms. The answer gives a general installed-cost range for the system type.

Both list the area's shared labor and fee rates, and, when enough listings exist, the state's published septic tank price. Send "Kind": "replacement" to include removing the old system.

Bulk feasibility (background job) ​

POST /feasibility/bulk screens up to 500 sites in one request (10 with a test key). Each site takes the fields of POST /feasibility (without ProjectId), and an optional Ref of your own that comes back with its row.

The answer is 202 Accepted with a job, and a Location header pointing at it:

json
{ "Id": "6f1c…", "Operation": "bulk-feasibility", "Status": "queued", "ItemCount": 250, "UnitsCharged": 2000,
  "StatusUrl": "/api/partner/v1/jobs/6f1c…", "ResultUrl": null }
  1. Poll GET /jobs/{id} (it sends Retry-After while the job runs) or wait for the job.completed webhook. Status goes queued, running, then succeeded or failed; ItemsDone shows progress.
  2. Read GET /jobs/{id}/result: one row per site, in the order sent, with Ok, Error and the same Feasibility answer as the single call. It answers 409 until the job has succeeded.

Units are per site and charged when the job is submitted (the plan's units must cover them all, or the answer is 402). Sites that can't be screened, such as an unknown state, are given back when the job finishes: a refund row with negative units appears in your usage. A retry with the same Idempotency-Key answers the same job and isn't charged again. Any key of the client can read the client's jobs. Jobs and their results are kept for 7 days.

Regulations ​

GET /regulations?state=OH&county=Franklin returns the area's septic rules: flow per bedroom, the minimum daily flow, system types (and whether each is permitted), setbacks and septic tank sizes by flow. The county is optional. It can be a name or a FIPS code.

Product recommendations ​

POST /products/recommendations ranks products for a spec at a site. Send a product type or category, the size range and the site.

  • Only eligible products: each product must be approved in the state when its type needs an approval, and a distributor must deliver it to the site.
  • Ranking: by fit, the number of distributors, stock and rating.
  • Sponsored products: paid placements come in their own Sponsored list, labelled. They are never mixed into the ranking.
  • Prices: no distributor's price is returned. PriceBand says whether the product's price is below, near or above the state's published median.

Catalog ​

EndpointReturns
GET /catalog/product-typesThe product types
GET /catalog/productsActive products. Filter by text, type, category, maker, the state it's approved in and size
GET /catalog/products/{id}A product, its state approvals, and how many distributors list it
GET /catalog/companiesManufacturers, distributors, installers and service companies (public profiles). Filter by role, state and verified
GET /catalog/companies/{id}A company, its public locations, and how many products it makes and lists

Market data (manufacturers and distributors) ​

EndpointReturns
GET /market/price-indexThe delivered-price index of a product type and size band, by month, state and county. A figure is published only when enough listings from enough distributors (5 and 3) stand behind it
GET /market/demandDesigns by county over the last months, for one system type if you choose, with the number of distributors that reach each county. Counties with fewer than 3 designs are hidden

Market data is aggregated. It never includes a company's own prices, or the designers and homeowners behind a design.

Quote requests (manufacturers and distributors) ​

When landplan.io sends a bill-of-materials quote request to your company, you can read it and reply through the API instead of the emailed link. A quote.requested webhook announces each new one.

EndpointDoes
GET /quotes?status=sentYour company's quote requests, newest first. Filter by sent, viewed, quoted or declined
GET /quotes/{id}One request: its lines (Items[].ItemId), the county and state, when it's needed, and your reply so far. Marks it viewed
POST /quotes/{id}/replyYour prices: Lines (ItemId, UnitPrice, Available, Note), DeliveryFee, ValidUntil, Note; or "Declined": true. You can change a reply until the request expires (30 days after it was sent)

A quote request shows the site's county and state, never the buyer or the address. A request of another company answers 404; an expired one 410; a closed or declined one 409.

Price lists (catalog:write) ​

POST /catalog/price-lists sends your price list and stock: Rows of ModelNumber (or Name), MakerName, PriceDelivered, PricePickup, StockQty, StockStatus, Currency, StockUnit and LeadTimeDays, for one of your locations (LocationId, your primary location when empty). Up to 5,000 rows.

The rows are matched to the catalog and staged as an import: nothing is published until your company approves the changes in landplan.io (My inventory > Import and review). The answer counts the rows that matched, the new products proposed, the rows that matched nothing and those that change nothing. GET /catalog/price-lists/{id} shows whether it has been reviewed.

Webhooks ​

landplan.io can POST events to your systems. Set it up under Webhook on the client's card on the API keys page (or ask us to):

  1. Type the Endpoint URL: an https address on the public internet.
  2. Tick the events you want, and leave Send events on.
  3. Click Add webhook. The signing secret is shown once: click Copy, then Done.

Once it is set up, the card has Save webhook, Send test, Rotate secret and Remove, and lists the deliveries.

EventSent whenData
job.completedA background job finished, succeeded or failedThe job, as GET /jobs/{id} answers it
quote.requestedA quote request was sent to your companyId, Title, State, NeededBy, ItemCount, SentUtc, ExpiresUtc, Url
pingYou pressed Send testA message

Every delivery is a JSON body:

json
{ "Id": "evt_9b2e…", "Type": "job.completed", "CreatedUtc": "2026-10-09T18:30:00Z", "ClientId": "…", "Data": { … } }

with these headers:

HeaderValue
LandPlan-Signaturet=<unix seconds>,v1=<signature>
LandPlan-EventThe event type
LandPlan-Event-IdThe event's Id: the same on every retry and redelivery, so you can drop repeats
LandPlan-DeliveryThis delivery's number

Verifying the signature ​

Each client has its own signing secret (whsec_…). It is shown once, when the webhook is made or the secret is rotated. To check a delivery:

  1. Split LandPlan-Signature on ,: t is the timestamp, each v1 a signature.
  2. Compute HMAC-SHA256 with the secret as the key over <t>.<raw body>, in lower-case hex.
  3. Accept the delivery if it equals any v1 (compare in constant time) and t is within 5 minutes of your clock. Reject anything else: that stops forged and replayed deliveries.
js
import crypto from 'node:crypto'

function verify (header, rawBody, secret) {
  const pairs = header.split(',').map(part => part.trim().split('='))
  const t = Number(pairs.find(([name]) => name === 't')?.[1])
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
  const expected = Buffer.from(crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'))
  return pairs.filter(([name]) => name === 'v1').some(([, value]) => {
    const given = Buffer.from(value || '')
    return given.length === expected.length && crypto.timingSafeEqual(given, expected)
  })
}

Use the raw body as received, before any JSON parsing. When you rotate the secret, the old one keeps signing (a second v1) for 24 hours, so you can switch over without missing a delivery.

Delivery and retries ​

  • Answer with any 2xx within 10 seconds. Do the work afterwards.
  • Anything else, a timeout or an unreachable endpoint is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: 8 attempts in all. Then the delivery is given up.
  • Redirects are not followed. Endpoints on private, local or cloud metadata addresses are refused.
  • The Webhook card lists every delivery of the last 30 days (Sent, Event, Status, Last attempt). Redeliver sends one again.

Plans and billing ​

Each operation costs a set number of units. Your plan includes units each month.

OperationUnits
Feasibility10
Design20
Estimate5
Product recommendations2
Price index2
Demand5
Regulations1
Catalog1
Bulk feasibility, per site8
Price list5
Quotes and replies, GET /me, GET /jobs/{id}0
PlanMonthly feeIncluded unitsBeyond themCalls a minuteCalls a day
Free$0100Stops (402)10100
Starter$992,000$0.10 a unit301,000
Pro$49915,000$0.06 a unit12010,000
EnterpriseBy contract100,000$0.04 a unit600100,000
  • Units are charged only for a successful call, once per Idempotency-Key. Test keys are never charged.
  • Every call is recorded with its key, operation, status, units and trace id. You can see your use on the API keys page.
  • We invoice monthly: the plan's fee plus any units beyond the included ones.
  • Each key can have its own limits: calls a minute, calls a day, a monthly unit quota, an expiry date and an allow-list of addresses.

Land & Septic Planning Software