Getting started
Which way is for you? WordPress: the plugin. Shopify, Squarespace, Webflow or any site: make your map under Maps, then Add to your site gives you the code. An app or your own code: one tag or the SDK, below. An AI assistant: connect it and ask.
Ways in: the WordPress plugin, one script tag, the SDK for a bundled app, the React Native package, or a secret key on your server. Each gives the browser a thirty-minute token that opens the tile host; the page renews it for free for a day. One visit is one map load. For a PNG from a URL instead of an interactive map, see the .
WordPress (no code)
The plugin puts maps on pages and posts without code: a Map block with up to 5,000 pins a map, a Store locator block, Map locations with CSV import and export, and the [deltabreen_map] and [deltabreen_store_locator] shortcodes for classic pages and page builders. WordPress 6.3 or later, PHP 7.4 or later.
- In WordPress, Plugins → Add New: search "Maps by DeltaBreen", install and activate.
- Settings → Maps by DeltaBreen → Connect: sign in or sign up and approve, and your site gets a publishable key of its own. Or paste a key from Keys that lists your site's domain.
- Add the Map or Store locator block, or a shortcode such as
[deltabreen_map center="-6.26,53.35" zoom="13" marker="true"].
A map made in the dashboard shows on WordPress too, on the plugin's own key: in the Map block open A map from your dashboard and paste the map's id or its address, or use [deltabreen_map map="<the map's id>"] (plugin 0.4.1 or later). Its pins, look and settings come from the dashboard, and edits you save there show within a minute.
When a map does not show, Check my key on the settings page says what to fix. Each page view with a map is one load; a store-locator search counts one search, and suggestions while typing are free.
After you switch it on, Get started asks what the map is for and opens a draft page made for it: a contact page, a store locator, all your places with a list, delivery areas, an event venue or a trail (the same patterns are in the block inserter). The Map block keeps the basics up front and the rest under More options: a list beside the map, a search box, directions drawn on the page, a GPX, KML or GeoJSON file, the buttons, how the map takes the page's scrolling, asking before it loads, your colour, and light or dark. Settings → How maps behave sets the same for the whole site, including “Ask before showing maps” for cookie banners. Map locations get a week editor for their hours with breaks and special days, a temporary closure and a notice, and a symbol in the pin; the Opening hours and Location card blocks show a location without a map. Developers get the deltabreen_maps_pins, deltabreen_maps_location, deltabreen_maps_post_pin and deltabreen_maps_attributes filters, and an optional public list of your locations at /wp-json/deltabreen-maps/v1/locations, which a map's data-maps-pins-url can read.
Moving from another plugin? Map locations → Import and export copies the places in WP Store Locator, WP Go Maps, MapPress or Agile Store Locator, with categories and hours, and reads a Google My Maps export (KML or KMZ); the other plugin's data is never changed. Settings → Location pages and posts gives each location a page of its own with the schema.org data search engines read, and lets posts of any type carry a place (a place box, an ACF Google Map or OpenStreetMap field, or two custom fields) for maps and store locators to show. You can click the Map block's preview to add pins or draw lines and areas. Maps that ask first follow Complianz, Borlabs Cookie, CookieYes or Real Cookie Banner, and Elementor has Map and Store locator widgets.
Maps made in the dashboard (no code)
For Squarespace, Webflow, Shopify or any site that takes HTML (on WordPress, the plugin shows it: see above): make the map under Maps and paste one tag. Add pins by address, by clicking the map or from a CSV file (the WordPress plugin's columns: name, address, city, postcode, country, latitude, longitude, phone, website, category, hours, notes, photo, button, button_url), give each category a colour, choose the look, and turn on the store locator, category buttons or a "where am I" button. Then open Add to your site, type your site's address (a key is made for it, or the one it has is used again) and copy the tag:
<div data-maps-key="pk_…" data-maps-map="<your map's id>" style="height: 480px"></div>
<script src="https://tiles.deltabreen.eu/embed.js" async></script>- Squarespace: add a Code block where the map goes and paste the tag. Scripts in a Code block need a plan that allows custom code.
- Webflow: drag a Code Embed element onto the page, paste the tag, then publish; the map shows on the published site.
- Shopify: Online Store → Themes → Customize, add a Custom Liquid section or block, paste the tag and save.
- Any site: paste the tag into the page's HTML; the script line is needed once per page.
A new map can start from a template: a contact page, a store locator, delivery areas, an event, a walk or trail, properties, or restaurants and cafés, with sample places to move or replace. In the editor each pin can carry one of our symbols, opening hours set day by day with special days such as Christmas, and a "Closed until" date with a notice; Lines and areas draws routes and zones on the preview, or brings in a GPX, KML or GeoJSON file, and More options sets the buttons, scrolling, a "Show map" consent step, your colour, dark panels, a list beside the map, a search box and directions drawn on the page.
No website to paste into? Share as a page gives the map its own link, a QR code for print, and frame code for Google Sites and Notion, framed only on the sites you list. Each view counts one map load, against one of your keys; a workspace with none gets one when it first shares a map (“Shared map pages”, which lists no sites). Preview shows the real map in a desktop or a phone frame before anyone sees it, and Saved versions keeps the last 20 saves, any of which you can bring back.
Keep your places in a Google Sheet? On Lite and above, From a Google Sheet links it: share the sheet with anyone who has the link (or publish it to the web as CSV) and paste the link. The sheet's rows become the pins, read every 15 minutes with the same columns as the CSV import, and a row with an address but no coordinates is looked up once, one search. Read it now asks for a read at once; rows that cannot be used are listed by line.
Visitor stats (Lite and above, under Visitor stats): for each map, views per day, the places visitors open and the buttons they press (directions, website, calls and your own), and the towns they search in a store locator, with where they found nothing near, a sign of where to open next. The map counts in the visitor's browser and sends only the counts when they leave the page: no address, nothing typed, no cookie. Maps on the same key that were not made in the dashboard (the WordPress plugin's, a page's own) are counted under the key. For your own analytics the map sends a deltabreen:place event when a place opens and deltabreen:search after a locator search (with the town picked and how many places were found), and its buttons carry data-maps-action (website, directions, route, call, button, list), on every plan:
document.querySelector('[data-maps-key]').addEventListener('deltabreen:place', (e) => {
gtag('event', 'map_place', { place: e.detail.title }) // or your own analytics
})Edits you save in the dashboard show on the site within a minute, without pasting again. Any data-maps-* attribute on the tag wins over the saved setting, so one map can open in another look or place on one page. Reading the map's pins and settings counts nothing; each page view is one map load, as with any map, and an address looked up while you build counts one search. Add your site's preview address to the key as well (such as your-shop.myshopify.com) to see the map before your domain is connected. A deleted map shows a short note in its place. How many maps and pins each plan holds is on the pricing page.
1. One tag (no code)
Get a publishable key from Add to your site → App or code (or under Keys), listing the sites that may use it, then put a map element and our script anywhere in the page. Works in any CMS block that allows HTML.
<div data-maps-key="pk_…"
data-maps-style="meadow"
data-maps-center="-6.26,53.35"
data-maps-zoom="12"
style="height: 480px"></div>
<script src="https://tiles.deltabreen.eu/embed.js" async></script>data-maps-style is a built-in look (meadow, midnight, neon, vintage, blueprint, sakura), a published style URL, or auto (the default: meadow on a light page, midnight on a dark one). Optional: data-maps-marker (empty for the centre, or lng,lat), data-maps-bearing, data-maps-pitch, data-maps-interactive="false", data-maps-paused-message, data-maps-language (a code such as de, ja or zh-Hans, or auto for the visitor's browser language). Several elements make several maps; one script serves them all. The script bundles MapLibre, its stylesheet and its worker, and handles the token exactly as the SDK does. A mistake in the attributes is written into the element, not swallowed. For a handle on a map, window.deltabreenMaps.mount(element) resolves to a maplibregl.Map; mountAll() picks up elements added later. Pin a version with https://tiles.deltabreen.eu/embed/v0.5.0.js.
With no settings the map fits the page it sits on. It has zoom and full-screen buttons (data-maps-controls: any of zoom, fullscreen, scale, or none). On a page that scrolls, two fingers or Ctrl / ⌘ with the wheel move it, so visitors scroll past it freely (data-maps-gestures: auto, greedy for one finger, off for a still map). Buttons and links take the page's own colour, or data-maps-accent="#0a7cff". The panels turn dark on a dark page, or set data-maps-theme to light or dark. Its words follow the page's language in English, German, French, Spanish, Italian, Portuguese, Dutch and Polish. A map further down the page starts only when a visitor scrolls near it, so a map nobody reaches is not counted (data-maps-lazy="false" starts it straight away). data-maps-consent="click" shows a "Show map" button in the map's place, and nothing loads until the visitor presses it; a cookie tool can call window.deltabreenMaps.consent() instead.
As a custom element. For Vue, Svelte, Angular, React 19 and builders that take one, the same script defines <deltabreen-map>: each attribute is a data-maps-* one without its prefix, read when the element is added, and look is the map's style, as style is the element's own CSS (Vue needs isCustomElement for it, Angular CUSTOM_ELEMENTS_SCHEMA).
<deltabreen-map key="pk_…" map="<your map's id>" style="height: 480px"></deltabreen-map>
<deltabreen-map key="pk_…" look="midnight" center="-6.26,53.35" zoom="12" style="height: 400px"></deltabreen-map>
<script src="https://tiles.deltabreen.eu/embed.js" async></script>Many pins and a store locator
Put pins inside the one-tag element as JSON: a list, a GeoJSON FeatureCollection, or { "pins": [...], "categories": {...} } with a colour for each category. A pin takes lng and lat, and optionally title, text, address, phone, url, color, category and hours (OpenStreetMap style, such as Mo-Fr 09:00-17:30; Sa 10:00-14:00). Without a centre the map fits its pins, and above 50 it groups them. Popups are built from text only, so nothing in the data is ever read as HTML. Pins cost nothing beyond the map load.
<div data-maps-key="pk_…" data-maps-style="meadow" data-maps-filter style="height: 480px">
<script type="application/json" data-maps-pins>
{ "pins": [
{ "lng": -6.2603, "lat": 53.3413, "title": "Grafton Street", "category": "Shops", "hours": "Mo-Sa 09:00-18:00" },
{ "lng": -9.0535, "lat": 53.2729, "title": "Shop Street Café", "category": "Cafés", "phone": "+353 91 555 0103" }
],
"categories": { "Shops": "#e11d48", "Cafés": "#15803d" } }
</script>
</div>Or point data-maps-pins-url at a GeoJSON file. Options: data-maps-cluster (auto, true or false), data-maps-filter (category buttons over the map), data-maps-locate (a "where am I" button) and data-maps-directions (auto opens the visitor's own maps app, google or apple always that one, off for no Directions link).
A pin can also carry a photo (shown at the top of its popup) and a button of your own, button and buttonUrl (such as "Book a table"), on every plan. On Lite and above a pin draws your own image instead of the coloured pin: icon on the pin, or an "icons" object next to the pins with one image per category. The image must be on the page's own site or served with CORS, and is fitted into 40 pixels; the free plan draws the coloured pin.
On every plan a pin can show one of our symbols in its head: glyph on the pin, or a "glyphs" object with one per category, such as { "Cafés": "coffee", "Shops": "shop" }. The symbols are shop, food, coffee, bar, wine, pizza, ice-cream, grocery, shopping, gift, hotel, camping, home, office, work, repair, hairdresser, gym, pet, baby, health, doctor, pharmacy, school, library, book, museum, church, theatre, music, event, ticket, park, tree, beach, hiking, viewpoint, water, boat, ferry, airport, train, bus, bike, car, parking, fuel, charging, atm, wifi, phone, mail, info, star, heart, flag, camera, toilet, accessible, police, fire and recycling. Opening hours take dated days too, such as Mo-Sa 09:00-18:00; Dec 25-26 off; 2026 Dec 24 10:00-14:00, and the popup says when that changes next ("Closes in 20 min", "Opens tomorrow 09:00") with the whole week on a tap. closedUntil (YYYY-MM-DD) shows "Closed until 1 Nov" whatever the hours say, and notice adds a line such as "Closed for renovation".
Lines and areas. A GeoJSON LineString or Polygon among the pins (or a "shapes" list beside them) draws a route, a trail or a zone under the pins, in its color (or GeoJSON's stroke and fill), width, opacity and dash, with a title and text on a click. data-maps-file draws a GPX, KML or GeoJSON file from your site: a GPX track shows its length, climb and a height profile, and a Google My Maps export keeps its colours, with its layers as categories (for a KMZ, use the KML inside it). In a store locator, a named area tells visitors after their search whether they are inside it ("Inside Free delivery"). A map holds up to 500 shapes and 20,000 points.
More around the map. data-maps-list (left, right or below) puts the places in a list beside any map, with an "Open now" button; data-maps-search adds a box that finds any place, where suggestions are free and a pick counts one search. data-maps-directions="route" draws the way on your own page from the visitor's location or an address, by car, on foot or by bike, with the steps: each route counts one search (an address typed counts one more), in Europe, North America, South America, Australia and Oceania, and anywhere else the visitor's own maps app takes over. data-maps-pins-from=".store" makes pins of cards already on your page, such as a Webflow CMS list or a Shopify loop: each card carries data-lng and data-lat, and optionally data-title (else its heading), data-category, data-hours, data-glyph and the other pin fields, and a click on a card opens its pin. On a map with more than one pin each popup has "Copy link", and an address ending in #place=<pin id> opens that place; a store locator page opened with ?near=Dublin starts with that search.
Add data-maps-locator and the same element becomes a store locator: a town, postcode or address search, "Use my location", a radius, category buttons, and the places nearest first with the distance, today's hours, an "Open now" badge, a phone link, a website link and directions in the visitor's own maps app. Suggestions while typing are free; a visitor's pick counts one search. "Use my location", distances, filters and directions count nothing. Options: data-maps-units (km or mi), data-maps-radius (the starting radius), data-maps-country (the codes the search keeps to, such as ie,gb) and data-maps-timezone (the timezone the hours are in, such as Europe/Dublin). A "labels" object next to the pins translates the locator's words. On WordPress, the plugin's Map and Store locator blocks do all of this without code.
From a CMS list. In Shopify, a metaobject list of your shops becomes the pins with a Liquid loop; in Webflow, give each item of a Collection List the custom attributes data-lng, data-lat and data-title bound to its fields:
{% comment %} Shopify: a "store" metaobject with title, latitude, longitude and hours {% endcomment %}
<div data-maps-key="pk_…" data-maps-pins-from=".store" data-maps-list="right" style="height: 520px"></div>
{% for store in shop.metaobjects.store.values %}
<div class="store" data-lng="{{ store.longitude }}" data-lat="{{ store.latitude }}"
data-title="{{ store.title | escape }}" data-hours="{{ store.hours | escape }}"></div>
{% endfor %}
<script src="https://tiles.deltabreen.eu/embed.js" async></script>2. The SDK (publishable key)
Create a publishable key under Keys, list the hostnames that may use it, then:
npm install @deltabreen/maps maplibre-glimport { createMap } from '@deltabreen/maps'
import 'maplibre-gl/dist/maplibre-gl.css'
const map = await createMap({
container: 'map', // an element id or element
key: 'pk_…', // your publishable key
style: 'meadow', // a built-in look, or a published style URL
center: [-6.26, 53.35],
zoom: 11,
language: 'auto', // optional: labels in the visitor's language; omit for local names
})
// map is a maplibregl.Map: markers, layers, events as usual.
map.setLanguage('ja') // switch labels in place; your own layers stayThe SDK fetches a token, adds it to every tile, glyph and sprite request, renews it for free while the page stays open, and shows a quiet "map paused" notice if your plan is exhausted instead of a grey grid. Labels come in 41 languages (LANGUAGES lists them): pass language as a code or 'auto'; a name the map data lacks in that language falls back to the local one. Each map start is one load; on localhost the token is also kept in localStorage, so hot reload does not start a new load on every save. Built-in looks: meadow, midnight, neon, vintage, blueprint, sakura.
Bundling with Vite or esbuild? maplibre-gl 6 loads its web worker from a file next to its own module, which those builds do not copy; the map then shows its style but never a tile. Hand the SDK the bundled worker:
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url' // Vite
const map = await createMap({ container: 'map', key: 'pk_…', workerUrl })React and Next.js
@deltabreen/maps/react gives you a <Map> component, a useMap() hook and <Marker>. On the server it renders an empty container and the map loads in the browser, so it works in Next.js without extra setup. The map is created once per key; changing its look, language, centre or zoom moves the same map, so none of that counts another load.
npm install @deltabreen/maps maplibre-gl
'use client'
import { Map, Marker } from '@deltabreen/maps/react'
import 'maplibre-gl/dist/maplibre-gl.css'
export default function StoreMap() {
return (
<Map apiKey="pk_…" mapStyle="meadow" center={[-6.26, 53.35]} zoom={12} style={{ height: 480 }}>
<Marker lngLat={[-6.26, 53.35]} popup="Our shop" />
</Map>
)
}apiKey is your publishable key (React keeps key for itself), mapStyle the look (style is the container's CSS), and a child calling useMap() gets the live MapLibre map for its own layers. onLoad, language, search and every createMap option work as props. With Vite, pass workerUrl as for createMap.
Already on react-map-gl? Keep it: point it at our style and add the token to each tile request. A token lives 30 minutes; for pages that stay open longer, <Map> and createMap renew it for free.
import Map from 'react-map-gl/maplibre'
import { requestToken, withToken } from '@deltabreen/maps'
const { token, tiles_url } = await requestToken({ key: 'pk_…' }) // one map load
<Map mapStyle={`${tiles_url}/styles/base/meadow.json`}
transformRequest={(url) => ({ url: withToken(url, tiles_url, token) })} />3. React Native
Native MapLibre on iOS and Android with the token handled for you. Create a publishable key and list the app as app:<bundle id> (the iOS bundle identifier and the Android package name; both if they differ). Wrap the app once, then use MapLibre's Map, Camera and layers as usual; several maps share one token, and one app launch is one load. language on the provider puts the built-in looks' labels into a language ('auto' follows the device); a published style keeps the language it was designed with.
npm install @deltabreen/maps-react-native @maplibre/maplibre-react-nativeimport { Camera, Map } from '@maplibre/maplibre-react-native'
import { MapsProvider, useMaps } from '@deltabreen/maps-react-native'
<MapsProvider apiKey="pk_…" appId="com.example.myapp">…</MapsProvider>
function Home() {
const { ready, paused, styleUrl } = useMaps()
if (paused) return <Text>Maps are paused for the month.</Text> // nothing is charged
if (!ready) return null
return <Map style={{ flex: 1 }} mapStyle={styleUrl('meadow')!}><Camera initialViewState={{ center: [-6.26, 53.35], zoom: 12 }} /></Map>
}A map made in the dashboard shows in one component (0.2 and later): its look, its pins with their colours, symbols and groups, its lines and areas, and a card for a tapped place with Open now and buttons for directions, the website and a call. usePlaces(id) and useLocator(places, { origin, radius, openNow }) build a list or a store locator of your own, and selected opens a place's card from that list and moves the map to it. Reading the map counts nothing; the app's token is its one load.
import { MapsProvider, SavedMap } from '@deltabreen/maps-react-native'
<MapsProvider apiKey="pk_…" appId="com.example.myapp">
<SavedMap id="<your map's id>" style={{ flex: 1 }} onPressPlace={(place) => …} />
</MapsProvider>Every tile, glyph, sprite and style request to the tiles host gets ?t= appended natively (MapLibre's TransformRequestManager, 11.0 or later), so style URLs stay plain and the edge cache works as on the web. Use tokenUrl instead of apiKey when your own server holds a secret key. Static images from an app: <Image source={{ uri, headers: imageHeaders }} /> with imageHeaders from useMaps(). Tokens carry the app id in an X-Maps-App header; like a browser origin it is a restriction, not a secret, and with no overage a copied key can only pause your map early.
Or let an assistant do it
Connect Claude Code, Claude, ChatGPT or Cursor to your account and ask for a key and a map: it creates the key with your site's hostname, publishes a style if you want one, and writes the snippet. It can also make a map in the dashboard from a spreadsheet you paste (addresses looked up for you), add lines and areas, turn on a store locator, share it as a page and give you the tag (create_map, update_map, list_maps, map_code). Same permissions as your dashboard role, every plan. Connect.
claude mcp add --transport http maps https://maps.deltabreen.eu/mcp4. A map link (any MapLibre app, no SDK)
One link is the whole map: a style whose tile addresses carry a token for the day. Give it to anything that takes a MapLibre style URL. Each fetch of the link counts one load, the same as a map start.
const map = new maplibregl.Map({
container: 'map',
style: 'https://maps.deltabreen.eu/api/v1/maps/meadow/style.json?key=pk_…',
})- Swap
meadowfor another look or a published style's id (its latest version, or&version=3), and add&lang=frfor labels in another language. - The key must list the site, as for any publishable key. An app with no web address names itself instead: add
app:com.example.appto the key's allowed sites and&app=com.example.appto the link, or send it as theX-Maps-Appheader. That is how Flutter and MapLibre Native apps on iPhone and Android use it: set the map's style URL to the link. - Add
&map=<id>for a map made in the dashboard: the style then carries its pins (in their colours, with their symbols and groups), its lines and areas, and opens on them; still one load a fetch. A tapped pin is in thedeltabreen-pinslayer with its title, address, hours and links as the feature's properties.GET /api/v1/site-maps/<id>/layersgives the same sources, layers and sprite sheets to add to a style of your own, uncounted. - On the free plan the style carries the “Maps by DeltaBreen” credit next to OpenStreetMap's.
- The link's tiles work for a day, so a map left open longer needs a reload. QGIS and other tools that only take a tile URL use raster tiles.
A tapped place, in Flutter (maplibre_gl), Swift and Kotlin:
// Flutter
final hits = await controller.queryRenderedFeatures(point, ['deltabreen-pins'], null);
if (hits.isNotEmpty) showPlace(hits.first['properties']); // title, address, hours, url, phone
// Swift (MapLibre Native)
let hits = mapView.visibleFeatures(at: point, styleLayerIdentifiers: ["deltabreen-pins"])
if let place = hits.first { show(place.attributes) }
// Kotlin (MapLibre Native)
val hits = map.queryRenderedFeatures(map.projection.toScreenLocation(latLng), "deltabreen-pins")
hits.firstOrNull()?.let { show(it.properties()) }For pages that stay open for days, mint the token yourself and renew it:
const r = await fetch('https://maps.deltabreen.eu/api/v1/token', {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ key: 'pk_…' }),
})
const { token, tiles_url } = await r.json()
const map = new maplibregl.Map({
container: 'map',
style: `${tiles_url}/styles/base/meadow.json`,
transformRequest: (url) => url.startsWith(tiles_url) ? { url: url + (url.includes('?') ? '&' : '?') + 't=' + token } : { url },
})
// Before expires_in seconds pass: POST { refresh: token } to the same endpoint and swap the token in.
// Mint once per page: each page that shows a map is one load, and renewals while it stays open are free.
// A refresh is honoured once per token; a second holder gets 403 token_reused and must mint (counted).Leaflet, OpenLayers and raster tiles
The same looks and published styles as PNG or WebP tiles. On a page with Leaflet or OpenLayers, use our plugin: it handles the token, so each map start is one load, exactly like the vector map, and the tiles come from Cloudflare's cache near your visitors. It works even on sites that hide their address from other sites. Load it after the library's own script:
<!-- Leaflet -->
<script src="https://tiles.deltabreen.eu/embed/leaflet.js"></script>
<script>
const map = L.map('map').setView([53.35, -6.26], 12)
L.tileLayer.deltabreen({ key: 'pk_…', style: 'meadow' }).addTo(map)
</script>
<!-- OpenLayers (the full ol.js build) -->
<script src="https://tiles.deltabreen.eu/embed/openlayers.js"></script>
<script>
new ol.Map({
target: 'map',
layers: [new ol.layer.Tile({ source: ol.source.deltabreen({ key: 'pk_…', style: 'meadow' }) })],
view: new ol.View({ center: ol.proj.fromLonLat([-6.26, 53.35]), zoom: 12 }),
})
</script>In a bundled app, the same from npm:
npm install @deltabreen/maps leaflet # or: ol
import { deltabreenTileLayer } from '@deltabreen/maps/leaflet'
deltabreenTileLayer({ key: 'pk_…', style: 'meadow', language: 'auto' }).addTo(map)
import { deltabreenSource } from '@deltabreen/maps/openlayers'
new TileLayer({ source: deltabreenSource({ key: 'pk_…', style: 'meadow' }) })- Options:
style(a built-in look, a published style's id, orid@3for one version),language(a code orauto),tileSize(512, the default, or256),format(webp, the default, orpng),tokenUrlinstead ofkeywhen your server holds a secret key, andpausedMessage/onPaused/onError. The layer also hassetStyle()andsetLanguage(). - The credit is added for you: OpenStreetMap, and on the Free plan “Maps by DeltaBreen” beside it.
- Nothing is minted until the layer is on a map, so a basemap in a layer switcher that nobody picks costs nothing.
Tools that only take a tile URL
QGIS, WordPress map plugins and anything else with a “tile URL” field: a publishable key goes in the URL instead of a token. The key must list the site's hostname, as for any publishable key, because browsers send the page's address with every tile. Every 50 of these tiles count as one map load, about what one visit to a raster map uses.
https://tiles.deltabreen.eu/raster/{style}/{256|512}/{z}/{x}/{y}[@x].{png|webp}?key=pk_…[&lang=fr]
// Leaflet without our plugin: {r} asks for @2x tiles on high-density screens, and .webp is about a third of .png's size
L.tileLayer('https://tiles.deltabreen.eu/raster/meadow/256/{z}/{x}/{y}{r}.webp?key=pk_…', {
maxZoom: 20,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map)
// 512-pixel tiles: a quarter of the requests for the same area
L.tileLayer('https://tiles.deltabreen.eu/raster/meadow/512/{z}/{x}/{y}{r}.webp?key=pk_…', { tileSize: 512, zoomOffset: -1, maxZoom: 20 }){style}is a built-in look (meadow,midnight,neon,vintage,blueprint,sakura), a published style's id (its latest version, within a minute of a publish) orid@3for one version.langis any of the label languages.- QGIS: add an XYZ connection with the URL above and put your site's address in its Referer field, or use a secret key there (never in a page).
- A page with a
no-referrerpolicy sends no origin: give the tile layerreferrerPolicy: 'strict-origin-when-cross-origin'(Leaflet 1.9 and later), or use our plugin, which needs no address. - On the Free plan, show “Maps by DeltaBreen” next to the OpenStreetMap credit, as the vector map does.
- Give raster use a key of its own, so the per-key usage shows it apart.
- Refusals are JSON with the usual codes: 400
bad_params, 401, 403origin_not_allowed, 402plan_exhausted, 404style_not_found, 429rate_limited.
5. From your server (secret key)
When you would rather not put a key in the page at all. Your backend holds the secret key and exposes a small route the page calls:
// Node / Express
app.post('/map-token', async (req, res) => {
const r = await fetch('https://maps.deltabreen.eu/api/v1/token', {
method: 'POST',
headers: { authorization: 'Bearer ' + process.env.MAPS_SECRET_KEY, 'content-type': 'application/json' },
body: JSON.stringify(req.body.refresh ? { refresh: req.body.refresh } : {}),
})
res.status(r.status).json(await r.json())
})# Python / FastAPI
@app.post("/map-token")
async def map_token(body: dict):
async with httpx.AsyncClient() as c:
r = await c.post("https://maps.deltabreen.eu/api/v1/token",
headers={"authorization": f"Bearer {os.environ['MAPS_SECRET_KEY']}"},
json={"refresh": body["refresh"]} if body.get("refresh") else {})
return JSONResponse(r.json(), status_code=r.status_code)// then in the page
const map = await createMap({ container: 'map', tokenUrl: '/map-token', style: 'midnight' })Never send a secret key from a browser: we refuse any request that carries one together with an Origin header.
Mint a token for each visitor (each one is a map load), never one token for everyone. On the Free plan we have measures in place that stop a shared token (it then answers 403 token_shared); on a paid plan we will reach out to you.
The token endpoint
POST https://maps.deltabreen.eu/api/v1/token
Authorization: Bearer sk_… (secret key) or body {"key": "pk_…"} (publishable, Origin checked)
body {"refresh": "<token>"} renew for free inside 24 h of the first token
200 { "token": "k_….…-…", "expires_in": 1800, "expires_at": <unix>, "refresh_until": <unix>,
"tiles_url": "https://tiles.deltabreen.eu", "counted": true, "phase": "normal", "attribution_required": false, "custom_pin_icons": true }
401 unknown or revoked key · 403 origin_not_allowed | origin_required | secret_key_in_browser
402 plan_exhausted: this month is used up (bank, surge and top-ups included); nothing was charged
429 too many tokens a minute for this keyUse the token on every request to the host the answer names as tiles_url, as ?t=<token>. A token opens that host and no other, and the host depends on the plan, so read it from the answer rather than writing it into your code; the SDKs do. The token is checked before any tile is served.
Styles
https://tiles.deltabreen.eu/styles/base/meadow.json a built-in look
https://tiles.deltabreen.eu/styles/<style-id>.json your style, latest published version (5-minute cache)
https://tiles.deltabreen.eu/styles/<style-id>/v3.json one version, immutable, cached for a yearDesign in the designer: pick a look, change the fourteen colour slots with a live preview, or paste any MapLibre style JSON. Publishing freezes a version; reference it by URL from the SDK or from plain MapLibre. Styles are plain MapLibre JSON on OpenStreetMap data (Protomaps build), yours to export.
On Studio and above you can upload your own custom icons (PNG pins and logos) and use them in a raw style's icon-image as custom:<name>.
Data layers
Your own data — store locations, delivery zones, boundaries — served as vector tiles over the map, on Studio and above. Upload a .geojson (points, lines or polygons) or a .csv with lat/lon columns under Data layers and it is tiled for you in the background (queued → converting → active, a minute or two for most files); a ready-made .pmtiles archive is served as-is. Each data layer is one vector source with one source-layer named after it:
https://tiles.deltabreen.eu/data/<dataset-id>/v1/{z}/{x}/{y}.mvtmap.addSource('shops', { type: 'vector', tiles: ['https://tiles.deltabreen.eu/data/<dataset-id>/v1/{z}/{x}/{y}.mvt'], minzoom: 0, maxzoom: 14 })
map.addLayer({ id: 'shops', type: 'circle', source: 'shops', 'source-layer': 'shops', paint: { 'circle-color': '#e11d48', 'circle-radius': 4 } })
// A second data layer is another source: its own id, its own tiles URL.
map.addSource('zones', { type: 'vector', tiles: ['https://tiles.deltabreen.eu/data/<other-id>/v1/{z}/{x}/{y}.mvt'] })
map.addLayer({ id: 'zones', type: 'fill', source: 'zones', 'source-layer': 'zones', paint: { 'fill-color': '#2563eb', 'fill-opacity': 0.3 } })The tiles are on your tile host, so the SDK and the one-tag embed sign the requests like any other tile; in plain MapLibre use the same transformRequest as for base tiles. Points want a circle layer, lines a line, polygons a fill; the Data layers page shows a ready snippet for each layer (MapLibre, SDK, or a fragment for a raw style JSON), with the right layer type. A new upload makes a new version at a new URL; the old one keeps serving until you delete the layer. A data layer is as public as the maps that show it: anyone with its address and a tile token can load its tiles, so keep private data out of it.
Every property in your file rides along into the tiles, and a converted layer gets a feature id per feature, so clicks, popups and hover states are ordinary MapLibre:
import { Popup } from 'maplibre-gl'
map.on('click', 'shops', (e) => {
const f = e.features?.[0]
if (!f) return
// Text, not HTML: a name from a data file or a search can carry markup.
const title = document.createElement('strong')
title.textContent = String(f.properties.name ?? '')
new Popup().setLngLat(e.lngLat).setDOMContent(title).addTo(map)
})
map.on('mouseenter', 'shops', () => { map.getCanvas().style.cursor = 'pointer' })
map.on('mouseleave', 'shops', () => { map.getCanvas().style.cursor = '' })
// Hover highlight: paint reads the feature state, the id comes with the tiles.
map.addLayer({ id: 'shops', type: 'circle', source: 'shops', 'source-layer': 'shops',
paint: { 'circle-radius': ['case', ['boolean', ['feature-state', 'hover'], false], 8, 5], 'circle-color': '#e11d48' } })
let hovered = null
map.on('mousemove', 'shops', (e) => {
const id = e.features?.[0]?.id
if (hovered !== null) map.setFeatureState({ source: 'shops', sourceLayer: 'shops', id: hovered }, { hover: false })
if (id !== undefined) map.setFeatureState({ source: 'shops', sourceLayer: 'shops', id }, { hover: true })
hovered = id ?? null
})Account API
Everything the dashboard does, from a script or a server-side bot: create map keys, publish styles, read usage. Create an API key under Settings → API & assistants (owner only, shown once, scoped) and send it as a bearer. The same key works on the connector at https://maps.deltabreen.eu/mcp.
# Who am I, and where does the month stand
curl -s https://maps.deltabreen.eu/api/v1/whoami -H "Authorization: Bearer $MAPS_API_KEY"
curl -s https://maps.deltabreen.eu/api/v1/account -H "Authorization: Bearer $MAPS_API_KEY"
# A publishable key for a site
curl -s https://maps.deltabreen.eu/api/v1/keys -H "Authorization: Bearer $MAPS_API_KEY" \
-H "content-type: application/json" \
-d '{"kind": "publishable", "name": "acme.ie", "origins": ["acme.ie", "*.acme.ie"]}'
# A style from a built-in look, then publish it for a URL that never changes
curl -s https://maps.deltabreen.eu/api/v1/styles -H "Authorization: Bearer $MAPS_API_KEY" \
-H "content-type: application/json" -d '{"name": "Brand", "base": "midnight"}'
curl -s -X POST https://maps.deltabreen.eu/api/v1/styles/<id>/publish -H "Authorization: Bearer $MAPS_API_KEY"# A map from a spreadsheet: rows without coordinates are looked up by address (one search each, never twice)
curl -s https://maps.deltabreen.eu/api/v1/site-maps -H "Authorization: Bearer $MAPS_API_KEY" -H "content-type: application/json" -d {"name": "Our shops", "csv": name,address,city
Gorey shop,1 Main Street,Gorey", "settings": {"locator: true}}
# Change a setting by name, add a pin; the rest stays as it was, and the replaced save is kept
curl -s -X PATCH https://maps.deltabreen.eu/api/v1/site-maps/<id> -H "Authorization: Bearer $MAPS_API_KEY" -H "content-type: application/json" -d '{"settings": {"list": "right"}, "add_pins": [{"lng": -6.46, "lat": 52.34, "title": "Wexford"}]}'Routes: GET /api/v1/whoami, GET /api/v1/account, GET|POST /api/v1/keys, PATCH|DELETE /api/v1/keys/:id, GET|POST /api/v1/styles, GET|PUT|DELETE /api/v1/styles/:id, POST /api/v1/styles/:id/publish, and maps made in the dashboard: GET|POST /api/v1/site-maps, GET /api/v1/site-maps/:id/saved, PATCH /api/v1/site-maps/:id (name, settings by name, pins, add_pins, remove_pins, csv, shapes, categories, glyphs, labels, public, frame_sites; owners and admins). Scopes: keys:read keys:write styles:read styles:write usage:read maps:read maps:write. Errors are { error, message } with stable codes. 120 requests a minute per key.
What counts, and what happens when you run out
- A map load is counted each time a page or an app shows a map. Panning, zooming and every tile it loads while it stays open are part of that one load; a map left open counts again only after 24 hours. A map from our one tag, the WordPress plugin or the map builder starts only when a visitor scrolls near it, so one further down a page that nobody reaches is not counted. Our Leaflet and OpenLayers plugins count one load per map start; raster tiles with a key in the tile URL (QGIS, WordPress map plugins) count 50 to a load.
- A search is an address or business search, a reverse lookup, an elevation request, a route or a travel-time area; a distance table counts one per pair. Type-ahead is free up to ten times the month's searches.
- Past the month's allowance: banked loads (20% of what you did not use last month, up to half an allowance), then a surge pool of half an allowance per calendar quarter, then any prepaid top-up loads, which never expire. Static images and searches follow the same steps on their own allowances, with their own top-ups.
- After that, a paid plan keeps running (the usage reports
grace) and a person from DeltaBreen emails you to find the right plan; normally nothing is paused without that conversation, though we reserve the right to limit access sooner if use threatens the platform's stability or runs up costs far beyond the plan (fair use). On Free, new loads pause until the month ends or you top up or upgrade. Maps already open keep working. - You are emailed at 50%, 80%, 100% and every step after; Studio and above can have the same events POSTed to a webhook.
- There is no overage charge and no way to incur one.
GET https://maps.deltabreen.eu/api/v1/usage Authorization: Bearer sk_… the month so far, as the dashboard shows itIf your site sets a Content-Security-Policy
A map fetches its token, style, tiles, fonts and icons from three hosts, so a strict policy has to allow all three. Which tile host a map is given depends on the plan, and it moves when the plan changes, so list both tile hosts even if you only see one today.
connect-src https://maps.deltabreen.eu https://tiles.deltabreen.eu https://tiles-direct.deltabreen.eu
img-src data: blob: https://tiles.deltabreen.eu https://tiles-direct.deltabreen.eu (the tile hosts for static images and Leaflet or OpenLayers tiles)
worker-src blob: (the one tag; with the SDK, wherever your bundler puts maplibres worker)
script-src https://tiles.deltabreen.eu (the one tag and the Leaflet and OpenLayers script tags)
style-src 'unsafe-inline' (the one tag, which adds the maps stylesheet itself)A map blocked this way shows nothing, and the browser's console names the address it refused.
With the SDK and a bundled worker file, the worker obeys the policy sent with its own file, not the page's. If that file is cached for long and you change the policy later, the worker keeps the old one until its address changes: give workerUrl a version, such as `${workerUrl}?v=${build}`, so each release fetches it afresh.