{"name":"geoprimitives","description":"Pay-per-call geospatial primitives. Priced in USDC over the x402 protocol. No accounts, no API keys, no subscription: every request carries its own payment. Read GET /openapi.json for request and response schemas.","network":"eip155:84532","payTo":"0x42b5882F9Fb35bC1014416F1637F351421151cb5","facilitator":"https://x402.org/facilitator","free":["GET /","GET /openapi.json"],"paid":[{"route":"POST /v1/hello","price":"$0.001","priceUsd":0.001,"summary":"Echo a string back. The cheapest way to test your payment client.","description":"Echo endpoint. Returns the name you send, the network the payment settled on, and the price you paid. Use it to prove your x402 client works end to end -- discover, read the 402, sign, retry, get a body -- before you spend anything on a real endpoint. It is the cheapest route in the catalog and it computes nothing, so a failure here is a payment problem and never a geospatial one. Do not use it for anything else: it has no geospatial behaviour at all.","tags":["test","x402","echo"]},{"route":"POST /v1/geojson/validate","price":"$0.005","priceUsd":0.005,"summary":"Check GeoJSON against RFC 7946 and say exactly what is wrong and where.","description":"Validate GeoJSON against RFC 7946 and get back a structured list of problems, not a bare boolean. Checks structure, coordinate range, ring closure, ring winding order, consecutive duplicate vertices, self-intersections, and hole nesting. Every issue carries the feature, geometry, ring and position index it sits on, so a 500-feature collection is actionable rather than merely condemned. Reach for it before paying for an operation on data you did not author, and to find out why /v1/geometry, /v1/h3/index or /v1/pip refused a document: those endpoints reject unclosed rings and out-of-range coordinates rather than guessing. Errors make a document invalid; warnings -- duplicate vertices, the crs member RFC 7946 removed -- do not, and are reported separately. Nothing is modified here: POST /v1/geojson/repair is the endpoint that rewrites. Limits: 1 MiB request body, and a self-intersection budget of 2000000 segment-pair comparisons per request, enough for any ring under about 1400 vertices. Rings past the budget are listed in checksSkipped with the reason, never silently passed as clean.","tags":["geojson","validation","rfc7946"]},{"route":"POST /v1/geojson/repair","price":"$0.01","priceUsd":0.01,"summary":"Rewrite broken GeoJSON into RFC 7946 form and log every change.","description":"Repair GeoJSON and get the fixed document back with a log of every alteration: rings rewound to RFC 7946 winding order, consecutive duplicate vertices dropped, unclosed rings closed, and polygon nesting fixed (mis-ordered exterior rings, holes that fall outside their exterior, MultiPolygon members that are really holes). Reach for it when POST /v1/geojson/validate reported errors, or when /v1/geometry, /v1/h3/index or /v1/pip refused your document -- unclosed rings are the usual reason, and this is the cheapest way to clear them. Conservative by design: it only rewrites the classes above, and anything else it found is returned in unrepaired rather than guessed at, so it will not invent geometry or move a coordinate. Idempotent -- a conformant document comes back untouched with changed:false, so a second call costs money and changes nothing. Your input object is never mutated. Coordinate values are preserved exactly; nothing is rounded or reprojected. Limit: 1 MiB request body.","tags":["geojson","repair","rfc7946"]},{"route":"POST /v1/transform","price":"$0.005","priceUsd":0.005,"summary":"Reproject coordinates or GeoJSON between 1236 EPSG codes.","description":"Reproject coordinates between EPSG coordinate reference systems. Accepts a bare position, an array of positions at any nesting depth, or a GeoJSON Feature, FeatureCollection or Geometry, and answers in whichever shape it was given, preserving properties, ids and foreign members. Covers 1236 codes: WGS 84 and every NAD83 realization as geographic, Web Mercator, all 120 WGS 84 UTM zones north and south, the NAD83 UTM zones, and the NAD83 State Plane and low-distortion zones, Oregon and Alaska included. Reach for it before calling /v1/geometry, /v1/h3/index or /v1/pip, which take EPSG:4326 only and refuse anything outside -180..180 and -90..90. Horizontal only: elevations pass through untouched. Accuracy, and read this before buying: no datum-shift grid is applied. proj4js cannot load NADCON or the NSRS realizations, so WGS 84 and every NAD83 realization are treated as one ellipsoidal datum. Measured against a full PROJ 9.5.1 pipeline that costs up to about 1.29 m on a WGS 84 to NAD83 pair -- fine for mapping, wrong for survey control -- and every response carries datumShiftApplied:false to say so. Projection arithmetic itself agrees with PROJ to under 1e-8 m. NAD27 codes are refused outright rather than approximated, because there the same simplification would cost tens of metres. A position that lands outside the target projection's valid domain is rejected with its index, never returned as a null or infinite coordinate. Limits: 10000 positions per call across the whole request, and a 1 MiB request body.","tags":["transform","reprojection","crs","epsg","proj4"]},{"route":"POST /v1/geometry/buffer","price":"$0.01","priceUsd":0.01,"summary":"Offset a geometry by a distance in metres, corrected for the ellipsoid.","description":"Buffer a geometry by a distance in metres and get the offset polygon back. Use it for service areas, setbacks, and proximity envelopes you will later test with POST /v1/pip. Accepts any geometry -- point, line, polygon, or a collection of them, each buffered about its own centre latitude -- and answers with a Feature, or a FeatureCollection when you sent several. A buffer that collapses (a negative distance larger than the shape, or any negative distance on a point or a line) answers geojson:null with empty:true, which is an answer and not an error. Geodesic-aware: turf buffers in an azimuthal equidistant plane scaled by a sphere, which is a systematic -0.56% error at the equator running to +0.44% at 80 deg latitude, and this endpoint cancels that by scaling the distance to the Gaussian radius of curvature at the feature's own centre latitude. Measured against GeographicLib the result is then within +/-0.34% of the requested distance at the equator, +/-0.08% at 60 deg, +/-0.04% at 70 deg and +/-0.01% at 80 deg; the residual is the ellipsoid's north-south versus east-west curvature, which no single sphere can represent, and it shrinks towards the poles. Between vertices the polygon edge cuts a further 1 - cos(pi/(4*steps)) inside the true offset curve, 0.054% at the default steps=24. Input is EPSG:4326 only. Limits: 2000 positions across the whole document, 1 MiB request body, distance within +/-1000000 m, steps 1 to 128.","tags":["geometry","polygon","buffer","turf"]},{"route":"POST /v1/geometry/simplify","price":"$0.01","priceUsd":0.01,"summary":"Thin out vertices with Douglas-Peucker, in metres or in degrees.","description":"Drop vertices from a geometry while keeping its shape. Use it to cut a boundary down before a call whose cost is vertex-driven -- POST /v1/pip caps on points times polygon vertices, and POST /v1/geometry/buffer and the overlay ops cap at 2000 positions -- and to shrink what you store or draw. The response reports outputVertices next to inputVertices, so the saving is measurable before you spend again. The algorithm is Douglas-Peucker. Send toleranceMeters for a ground tolerance applied isotropically: both axes are scaled to metres at the input's centre latitude first, so at 70 deg N the simplification is not three times as aggressive east-west as north-south, which a raw degree tolerance makes it. Send tolerance instead for the raw degree tolerance if you want turf's behaviour unchanged. Retained vertices come back exactly as sent rather than round-tripped through the scaling. Not topology-preserving: adjacent polygons simplified separately can gap or overlap along a shared edge. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.","tags":["geometry","polygon","simplify","douglas-peucker","turf"]},{"route":"POST /v1/geometry/union","price":"$0.01","priceUsd":0.01,"summary":"Merge two or more polygons into one shape.","description":"Dissolve overlapping or adjacent polygons into a single shape: merging parcels, coverage areas or service zones into one boundary before measuring or testing it. Send a FeatureCollection of Polygons or MultiPolygons; the answer is a single Polygon or MultiPolygon Feature, or null with empty:true when nothing is left. Clipping is planar in longitude/latitude, the way every polygon-clipping library does it: edges are straight in degrees rather than geodesics, and the two differ on edges spanning more than about a degree. The 2000-position cap is CPU rather than bandwidth: the sweep-line overlay is quadratic on self-intersecting input, where 2000 positions is already about 350 ms. Input is EPSG:4326 only. Limits: 2000 positions across all members, 1 MiB request body, and at least two polygonal geometries or the call is refused.","tags":["geometry","polygon","union","overlay","turf"]},{"route":"POST /v1/geometry/intersect","price":"$0.01","priceUsd":0.01,"summary":"Return the area two or more polygons have in common.","description":"Clip polygons to their common area: how much of a parcel falls inside a flood zone, where two service areas overlap, what a boundary and a study area share. Pair it with POST /v1/geometry/area to turn the overlap into square metres. Disjoint input answers geojson:null with empty:true, which is a real answer and not an error, so check empty rather than treating null as a failure. Send a FeatureCollection of Polygons or MultiPolygons; the answer is the common area as a single Feature. Clipping is planar in longitude/latitude, so edges are straight in degrees rather than geodesics. The 2000-position cap is CPU rather than bandwidth: the sweep-line overlay is quadratic on self-intersecting input. Input is EPSG:4326 only. Limits: 2000 positions across all members, 1 MiB request body, and at least two polygonal geometries or the call is refused.","tags":["geometry","polygon","intersect","overlay","turf"]},{"route":"POST /v1/geometry/difference","price":"$0.01","priceUsd":0.01,"summary":"Subtract later polygons from the first one. Order matters.","description":"Cut holes out of a polygon: the buildable part of a parcel once setbacks are removed, the land outside a protected area, the part of a route not already covered. Order is significant, so this is the one overlay op where swapping the operands gives a different answer. Subtracting everything answers geojson:null with empty:true, a real answer rather than an error. Send a FeatureCollection of Polygons or MultiPolygons; the first member in document order has every later member subtracted from it. The answer is a single Feature, or null with empty:true when nothing survives. Clipping is planar in longitude/latitude, so edges are straight in degrees rather than geodesics. The 2000-position cap is CPU rather than bandwidth: the sweep-line overlay is quadratic on self-intersecting input. Input is EPSG:4326 only. Limits: 2000 positions across all members, 1 MiB request body, and at least two polygonal geometries or the call is refused.","tags":["geometry","polygon","difference","overlay","turf"]},{"route":"POST /v1/geometry/area","price":"$0.01","priceUsd":0.01,"summary":"Ellipsoidal area of a polygon in square metres, holes subtracted.","description":"Measure how much ground a polygon covers, in square metres and square kilometres, with holes subtracted and every part of a MultiPolygon summed. Use it after POST /v1/geometry/intersect to size an overlap, or on a boundary you already hold. Points and lines contribute zero rather than an error, so a mixed collection is safe to send. Not turf's area, which applies the spherical-excess formula to geodetic latitudes on a 6371008.8 m sphere and comes out 0.45% high at the equator and 0.85% low at 80 deg latitude. This maps latitude to authalic latitude on the WGS 84 authalic sphere first, landing within 5e-3% of GeographicLib's geodesic area on parcel-scale rings and converging to 1e-6% as ring edges are densified. Ring edges are treated as straight in longitude/latitude, per RFC 7946 and turf; a geodesic-edge area differs on long slanted edges, by 1.9% on a 1.7-degree triangle. Winding order is ignored: ring 0 is the exterior and the rest are holes. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.","tags":["geometry","polygon","area","measurement","wgs84"]},{"route":"POST /v1/geometry/centroid","price":"$0.01","priceUsd":0.01,"summary":"True area centroid of a polygon, weighted correctly at high latitude.","description":"Find the area centroid of a polygon: a representative point for labelling, joining, or reverse geocoding a shape you only have a boundary for. It is the centre of mass of the area with holes subtracted, not the average of the vertices, so densely sampled edges do not drag it. Know what a centroid is not: on a concave or ring-shaped polygon the centre of mass can fall outside the polygon itself, and this endpoint returns it anyway rather than nudging the point inside. Test it with POST /v1/pip if you need a point that is guaranteed to be inside. Not turf's centroid, which is the arithmetic mean of the vertices and drifts towards whichever part of the boundary happens to be densely sampled. The moment integral is evaluated in a Lambert cylindrical equal-area projection of the WGS 84 authalic sphere, so the cos(latitude) area weighting is right at high latitude, then inverted back to longitude and latitude. Non-areal input, and polygons that enclose zero area, fall back to the vertex mean and say so in the method field. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.","tags":["geometry","polygon","centroid","measurement","wgs84"]},{"route":"POST /v1/geometry/bbox","price":"$0.01","priceUsd":0.01,"summary":"Extent of any geometry as [minLon, minLat, maxLon, maxLat].","description":"Get the extent of a geometry or a whole collection: a map viewport, a tile query window, a cheap prefilter before you pay for a containment test. Takes any geometry type, not just polygons. Bounding box of any geometry as [minLongitude, minLatitude, maxLongitude, maxLatitude]. Naive minimum and maximum per axis, so a geometry spanning the antimeridian reports a full-width longitude range rather than the narrow band it occupies; the response flags that case instead of guessing which side you meant. Input is EPSG:4326 only. Limits: 100000 positions across the whole document, 1 MiB request body.","tags":["geometry","bbox","extent","measurement"]},{"route":"POST /v1/h3/index","price":"$0.01","priceUsd":0.01,"summary":"Index points to H3 cells, or polyfill polygons with them.","description":"H3 hexagonal grid indexing, both directions of the common case. Use it to bucket points into a fixed grid for joining, aggregating or deduplicating across datasets that share no keys, and to turn a polygon into the set of cells that cover it so a spatial question becomes a set-membership question. Every call needs exactly one of points or geojson, and exactly one of resolution or resolutions; sending neither is a 400 you will have paid for. Send \"points\" to index positions to their containing H3 cell at one or several resolutions, optionally with each cell's centre and boundary ring. Send \"geojson\" to polyfill polygons instead: the set of H3 cells covering a Polygon or MultiPolygon at the requested resolutions, with a choice of containment rule. Polyfill cost is estimated before any cells are generated and refused above 100000 cells, because cell count grows about sevenfold per resolution step; set \"estimateOnly\" to size a job without generating it. Input is EPSG:4326, longitude then latitude. Limits: 10000 points, 4 resolutions per call, 500 polygons and 10000 polygon positions, 100000 cells per polyfill, and a 1 MiB request body.","tags":["h3","index","polyfill","grid","hexagon"]},{"route":"POST /v1/pip","price":"$0.01","priceUsd":0.01,"summary":"Test many points against many polygons and get the full matrix.","description":"Answer which of your points fall inside which of your polygons, in one call. Use it to tag events with the district, catchment or territory they belong to, to filter a point set down to a study area, or to join two datasets that share nothing but geography. The answer comes back twice, as a dense 0/1 matrix and as a per-point list of polygon indices, so neither shape needs rebuilding on your side, and polygon ids are echoed as column labels so the matrix joins straight back to your records. Rings must be closed and coordinates must be EPSG:4326: run POST /v1/geojson/repair first if they are not, and POST /v1/transform if they are not in degrees. Costs scale with polygon detail, not just polygon count, so POST /v1/geometry/simplify on the polygons is often the cheapest way under the caps. The matrix is dense: every point is tested against every polygon, one row per point and one column per polygon, 1 for inside and 0 for outside. A point exactly on an edge or vertex counts as inside unless \"ignoreBoundary\" is set. Two caps, both on products rather than on either side: at most 250000 point-polygon pairs, and at most 25000000 point-vertex tests, since a ray cast walks every edge. Input is EPSG:4326, longitude then latitude. Limits: 250000 points, 2000 polygons, 100000 polygon positions, and a 1 MiB request body.","tags":["pip","point-in-polygon","containment","matrix","wgs84"]},{"route":"GET /v1/timezone","price":"$0.005","priceUsd":0.005,"summary":"IANA timezone, current UTC offset and DST flag for a coordinate.","description":"Give it a longitude and latitude and it answers with the IANA timezone identifier (America/New_York), the UTC offset in force there right now, whether daylight saving is currently in effect and by how much, the local wall-clock time, and the zone's standard offset. Pass an optional ISO 8601 `at` instant to ask about any moment between 1900 and 2100 instead of now, which is what you want when scheduling something months out across a DST boundary. Parameters go in the query string: this is a GET, there is no body. Two halves, with different vintages, and the difference matters. The offset and the DST rules come from the runtime's own timezone database and are current: a rule change reaches this endpoint when Cloudflare updates ICU, with no redeploy here. The *boundaries* come from tz_world (efele.net), CC0 1.0 and derived from VMAP0, a public-domain US government product. That map was frozen at tzdb 2016d in May 2016, and this catalog sells public-domain and permissively-licensed data only, which is why it is used rather than the current OpenStreetMap-derived alternative. What the vintage costs, measured against IANA's own coordinates for all 312 zones in zone1970.tab rather than estimated: 293 agree. Of the 19 that do not, 7 are Antarctic research stations, which this map does not cover at all and which fall back to the open-water rule; 7 more return a different identifier for the identical offset and DST all year (Australia/Brisbane for Australia/Lindeman, America/Regina for America/Swift_Current, and five like them), so the time is right and only the name is old. That leaves four regions where the answer is up to one hour wrong because the zone was split after 2016: Chilean Magallanes and Aysen (answered as America/Santiago rather than America/Punta_Arenas or America/Coyhaique), the Mexican border municipalities around Ciudad Juarez, Saratov Oblast in Russia, and the weeks each spring when Asia/Hebron and Asia/Jerusalem change on different dates. If you query inside one of those, do not trust this endpoint. Boundary precision is a separate question from vintage. The map is indexed as compacted H3 cells at resolution 6, whose half-width is about 1.6 km, chosen to match VMAP0's own stated positional accuracy of roughly 2 km rather than to flatter the index. So a point within a couple of kilometres of a zone border may be attributed to the neighbour, and no amount of the source's precision would fix that because the source does not have it. Away from a border the answer is exact. Every answer says where it came from under `source`. `boundary` means the point is inside a mapped timezone polygon and is the answer you are paying for. `coastal` means it is over water or on land this map does not cover, and the identifier is the nearest zone within about 60 km -- correct for a harbour or a coastal city whose coordinate falls just offshore, and not a boundary result. `nautical` means open water more than about 60 km from mapped land, answered with the tz database's own rule for international waters, meridians 15 degrees apart; it is not the time kept aboard any vessel and it ignores territorial waters. Longitude and latitude are checked against EPSG:4326 bounds before anything else touches them, because H3 silently normalises an out-of-range latitude into a real but wrong cell rather than refusing it. Reproject with POST /v1/transform first if you are not already in WGS 84. The DST flag is decided on offsets rather than on the tz database's own isdst bit, so Europe/Dublin reads as daylight saving in summer like everywhere else, and it is measured over a window either side of the instant so that a zone which changes its standard offset mid-year -- Asia/Almaty in 2024 -- is not mistaken for one on daylight saving.","tags":["timezone","iana","dst","utc-offset","h3"]}]}