API reference

One call answers one question: for this address, what should happen. The answer always carries a reason code, because a decision you cannot explain is one you cannot act on.

The machine-readable description is at /openapi.json. It is generated from the same code the resolver runs, so the codes below are the codes you will actually receive.

Authenticating

Every request carries a key as a bearer token. Keys are created in Settings, shown once, and stored as a hash, so a lost key is replaced rather than recovered.

Keys begin ml_test_ or ml_live_, and carry scopes. A resolve needs the resolve scope and nothing more. Give an integration the narrowest set that does its job.

A test key is the same engine with the meters off: it resolves against your published configuration through the same code, is never billed or counted against your plan, and rate limits separately, so developing cannot starve production. Build againstml_test_, swap the key to go live.

Deciding an address

Send an address or a coordinate. An address is geocoded first. A coordinate is used as given, which is what makes an answer reproducible.

Requestbash

curl https://api.maplogics.com/v1/resolve \
  -H "Authorization: Bearer ml_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "address": {
      "line1": "2 E Main St",
      "city": "Richmond",
      "region": "VA",
      "postal_code": "23219",
      "country": "US"
    },
    "context": { "service": "hvac" }
  }'

Responsejson

{
  "request_id": "req_9039f987",
  "status": "resolved",
  "reason_code": "ASSIGNMENT_RULE_MATCHED",
  "serviceable": true,
  "territory": { "id": "trr_...", "name": "Richmond Central" },
  "location":  { "id": "loc_...", "name": "Richmond Central" },
  "outputs": {},
  "ruleset_version": "cfg_01M0X5..."
}

Unserviceable is a 200. The question was answered and the answer was no. A non-2xx means we could not answer, which is a different thing and usually your retry.

Try it from here

Paste a test key and send a real request to the API from this page. Test keys work from the documentation without any origin setup, resolve against your published configuration, and are never billed or metered. A live key is refused from a browser unless its origin is allow-listed, which is the point of live keys.

The key stays in this page's memory and goes only to the API, in the same header your code will send. Nothing here stores it.

From an AI agent

The same decisions are offered as tools over the Model Context Protocol athttps://api.maplogics.com/mcp, with the same key in the same header. Claude and other MCP clients can check an address, check a coordinate and explain a decision. TheMCP guide has the setup for each client.

Reason codes

Branch on reason_code, never on message. The message is written for a person and will be reworded; the code is the contract.

resolved

PRIMARY_ELIGIBLE_TERRITORY
Resolved to the highest-priority eligible territory.
ASSIGNMENT_RULE_MATCHED
An assignment rule selected this territory explicitly.
FALLBACK_ASSIGNED
No primary candidate could take it, so the configured fallback did.

unserviceable

ALL_CANDIDATES_INELIGIBLE
A territory covers this address, but no assigned location can perform the requested service. Check the capabilities on those locations.
EXCLUDED_BY_RULE
Every candidate was removed by an exclusion rule.

no_coverage

NO_TERRITORY_COVERS_POINT
No territory covers this address. Extend a boundary or add one to cover it.

ambiguous

EQUAL_PRIORITY_OVERLAP
Two or more territories are equally eligible and nothing breaks the tie. Set an explicit priority, or narrow one boundary.

invalid_input

ADDRESS_NOT_FOUND
No geocoder could place this address. Check it for typos, or send a coordinate instead.
ADDRESS_TOO_IMPRECISE
This address could only be placed to a postal code or region, which is not precise enough to route: a boundary can run through it. Add a street number, or send a coordinate.
INVALID_INPUT
The request could not be understood.

Never collapse these into a boolean. "No territory covers this address", "a territory covers it but nobody can do the work", and "two territories tie" have three different remedies. A single false throws away the one thing that tells you which.

Errors

Every error carries a stable code, the same request_id as the response header, and a message for a person.

CodeHTTPMeaning
invalid_request400The request body could not be understood.
invalid_coordinate400lat must be between -90 and 90 and lng between -180 and 180. Note the order: [lng, lat].
missing_location_input400Provide either an address or a coordinate.
unauthorized401Provide an API key as "Authorization: Bearer ml_live_...".
invalid_api_key401That API key is not valid. Keys are shown once at creation and cannot be recovered.
revoked_api_key401That API key has been revoked.
invalid_token401That access token is not valid or has expired. Connect again from your AI client to sign in.
organization_suspended403This organization is not active. Contact support to restore access.
forbidden403Your role in this organization does not allow this action.
forbidden_origin403This key is not allowed to be called from that origin.
insufficient_scope403That key does not carry the scope this endpoint needs.
no_published_configuration409This organisation has not published a configuration yet, so there is nothing to resolve against.
conflict409Someone else changed this since you loaded it. Reload to see their version.
precondition_required428This change would overwrite an existing record. Send the version you are editing so we can check nobody else changed it first.
idempotency_conflict409That idempotency key was already used with a different request body.
rate_limited429Too many requests. Retry after the interval in the Retry-After header.
quota_exceeded429This organization has used the decisions its plan includes this month. The count resets at midnight UTC on the first of next month. Plan limits are at https://app.maplogics.com/billing.
geocoder_unavailable502The address could not be geocoded because every provider failed. Retry, or send a coordinate.
runtime_unavailable503The configuration for this organisation could not be loaded. This is on our side.
not_found404No record with that identifier exists for this organization.
internal_error500Something went wrong on our end. Quote the request id if you get in touch.

Webhooks

Rather than polling for changes, subscribe. We send a signed POST when a configuration goes live, when one is rolled back, and when a location is archived.

Each delivery carries X-MapLogics-Signature: t=<unix>,v1=<hex>. Verify it before trusting the body: your endpoint is public, and without the check anyone who learns the URL can tell you your routing changed.

Verifying a deliveryjavascript

import crypto from 'node:crypto'

// The timestamp is inside the signed material, so it cannot be
// edited to make an old body look current.
export function verify(secret, rawBody, header) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('=')),
  )
  const age = Math.abs(Date.now() / 1000 - Number(parts.t))
  if (age > 300) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')

  // Constant time: comparing with === leaks where the first
  // difference is, which over many attempts is enough.
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1),
  )
}

Answer 2xx once you have stored it. Anything else is retried with widening backoff over about two hours, and every attempt is visible in Settings with the status we got back.