Developers

Co-branded Reports API

Create MapView property and suburb reports with your branding, straight from your CRM or your own app.

Overview

The Co-branded Reports API creates MapView PDF reports that carry your organisation’s branding. Send an Australian address or a suburb, and MapView produces a report with:

  • your logo on the cover, on interior page headers and in every footer
  • your display name and a footer strapline
  • a named contact person on the cover and in every footer
  • MapView attribution, which stays in every footer.

There are two report types:

TypereportTypeYou send
Property report2A validated Australian property address, with an optional unit
Suburb report3A suburb, state and postcode, and a region for Queensland

Reports are made asynchronously. You create a job, then poll it or receive a signed webhook when it’s ready. The finished PDF is downloaded through an authenticated endpoint.

Base URL

https://api.mapview.ai/api/v1/cobranded

The machine-readable OpenAPI description is at api.mapview.ai/docs (JSON).

Don’t want to build an integration? Your team can create the same reports in the MapView partner portal without writing code.

Before you start

Co-branded reports are free. Where online sign-up is available, sign up and confirm your email: your API key appears straight away, and you can add your branding on the same page. Otherwise, get in touch and we’ll arrange:

  • An organisation with a free monthly report quota.
  • An API key. It starts with mv_live_ and is shown to you once. Store it in your secret manager straight away.
  • Your branding. This covers your logo, display name, footer strapline and contact person. See Branding.
  • A webhook endpoint, if you want push notifications. This is optional. See Webhooks.
  • A test organisation, if you want one. It runs on the production API and uses its own quota.

Quickstart

Four calls take you from an address to a PDF. Each needs your key in the Authorization header.

1. Check the address

curl -sS -X POST https://api.mapview.ai/api/v1/cobranded/addresses/resolve \
  -H "Authorization: Bearer $MAPVIEW_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"address":"19 Gresford Road, Wantirna VIC 3152"}'

2. Create the report

curl -sS -X POST https://api.mapview.ai/api/v1/cobranded/property-reports \
  -H "Authorization: Bearer $MAPVIEW_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"address":"19 GRESFORD ROAD, WANTIRNA VIC 3152","externalReference":"crm-12345"}'

The response is 202 Accepted with a report id and "status": "queued".

3. Poll until it’s ready

curl -sS https://api.mapview.ai/api/v1/cobranded/reports/$REPORT_ID \
  -H "Authorization: Bearer $MAPVIEW_PARTNER_KEY"

4. Download the PDF

curl -sS https://api.mapview.ai/api/v1/cobranded/reports/$REPORT_ID/pdf \
  -H "Authorization: Bearer $MAPVIEW_PARTNER_KEY" \
  -o report.pdf

Authentication

Every partner endpoint takes your key as a bearer token:

Authorization: Bearer mv_live_...

Each key belongs to one organisation and carries the reports:read and reports:write scopes. Every report, PDF, delivery and usage call is restricted to that organisation automatically. A report ID that belongs to another organisation returns 404 not_found.

You can have more than one key, for example one for production and one for CI. A revoked key fails immediately with 401 unauthorized. Ask us to issue or revoke keys.

Keep the key on your server

The key is a long-lived secret. Never put it anywhere a user can read it. That rules out:

  • browser JavaScript
  • a mobile app bundle
  • an iframe or link URL
  • local storage, cookies or analytics
  • a public repository.

If people use your integration from a web page, have the page call your own backend. Your backend adds the key and calls MapView:

Browser (your login / session)
   ↓  your own endpoints, e.g. POST /api/reports/property
Your backend (holds the mv_live_ key)
   ↓  Authorization: Bearer mv_live_...
https://api.mapview.ai/api/v1/cobranded

Return only the fields your page needs. Requests straight from a browser to the partner API aren’t supported: partner origins aren’t allowed by CORS, and there is no browser-safe token in this release.

Report lifecycle

statusMeaning
queuedAccepted and waiting for a renderer
generatingThe PDF is being produced
readyThe PDF is available
failedTerminal failure. The report’s quota is refunded automatically

A property report usually takes 30 to 60 seconds, and a suburb report 60 to 120 seconds. Don’t hard-code these times. A render that runs longer than four minutes fails, and jobs that stall are cleaned up within 15 minutes.

brandingApplied

This is null until the report is ready, then true or false.

  • true: your branding and contact person were applied.
  • false: your branding couldn’t be applied, for example because the logo couldn’t be read, so the standard MapView report was produced. This is still a valid, ready report, not a failure. Show it as “Standard MapView report” rather than as an error.

The report object

Every create, poll and list call returns reports in this shape:

{
  "id": "377400d8-a07f-4cad-ad55-1b3328156bda",
  "status": "queued",
  "reportType": 2,
  "externalReference": "crm-12345",
  "address": "19 GRESFORD ROAD, WANTIRNA VIC 3152",
  "unit": null,
  "suburb": null,
  "state": "VIC",
  "postcode": "3152",
  "regionId": 2,
  "brandingApplied": null,
  "reportUrl": null,
  "pollUrl": "https://api.mapview.ai/api/v1/cobranded/reports/377400d8-a07f-4cad-ad55-1b3328156bda",
  "error": null,
  "createdAt": "2026-10-21T05:15:36.048Z",
  "completedAt": null
}

When it’s ready:

{
  "status": "ready",
  "brandingApplied": true,
  "reportUrl": "https://reports.mapview.ai/cobranded/1/377400d8-a07f-4cad-ad55-1b3328156bda.pdf",
  "error": null,
  "completedAt": "2026-10-21T05:15:45.771Z"
}

When it has failed:

{
  "status": "failed",
  "brandingApplied": null,
  "reportUrl": null,
  "error": { "code": "pdf_unavailable", "message": "Renderer is temporarily unavailable" }
}

reportUrl is set only when status is ready. It’s an unguessable object URL, so treat it as a secret link. Anyone who has it can open the PDF. Within an app that requires a login, download through the PDF endpoint instead.

Addresses

Resolve an address

POST /api/v1/cobranded/addresses/resolve

This checks an address against G-NAF, Australia’s national address register, and returns the canonical address and its region. Resolve every address before you create a property report.

{ "address": "19 Gresford Road, Wantirna VIC 3152", "unit": "11" }

If your system already stores G-NAF IDs, send addressId instead of address. This skips geocoding and looks the address up directly:

{ "addressId": "GAQLD160168662", "unit": "11" }

A successful response:

{
  "matched": true,
  "match": "exact",
  "address": {
    "formatted": "19 GRESFORD ROAD, WANTIRNA VIC 3152",
    "placeId": "GAVIC...",
    "lat": -37.8,
    "lng": 145.2,
    "unit": "",
    "state": "VIC",
    "postcode": "3152",
    "locality": "WANTIRNA",
    "unitVerified": false
  },
  "regionId": 2,
  "candidates": ["19 GRESFORD ROAD, WANTIRNA VIC 3152"],
  "buildingNameIgnored": false
}

match is one of these values. A match on the street alone (no street number) is rejected with 422 street_only_match.

matchMeaning
exactThe address matched exactly
unit_exactThe address and the unit matched exactly
rangeThe street number falls within a numbered range, such as 2–8
building_onlyThe building matched, but not the unit

Before you create the report, show the user address.formatted and the unit so they can confirm them.

Units

Send the unit in its own unit field, not in the address. If the address string also contains a unit, the two must match; otherwise you’ll get 422 unit_conflict. unitVerified says whether the unit was found in G-NAF.

What isn’t supported

PO Boxes, and LEVEL, LOT, REAR and OFF addresses, return 422 unsupported_address_format.

Autocomplete

Address autocomplete isn’t part of the partner API. You can use any autocomplete you like, but always send the chosen address through resolve before you create a report.

When there are several possible matches

postcode_mismatch, ambiguous_address and street_only_match errors include a candidates list. Show the candidates as options, then send the one the user picks back through resolve.

Create a property report

POST /api/v1/cobranded/property-reports
Authorization: Bearer <key>
Content-Type: application/json
Idempotency-Key: <uuid-v4>
{
  "address": "19 GRESFORD ROAD, WANTIRNA VIC 3152",
  "unit": null,
  "externalReference": "crm-12345",
  "regionId": 2
}
FieldRequiredNotes
addressaddress or addressIdFree-text Australian address. Ideally the formatted value from resolve
addressIdaddress or addressIdG-NAF ID. Skips geocoding
unitNoUnit identifier, separate from the address
regionIdNoOverrides the region. Must belong to the address’s state
externalReferenceNoYour own ID (CRM, listing or job). It’s returned with the report and can be used to look reports up
callbackUrlNoAn HTTPS webhook URL for this report only. Signed with your organisation’s webhook secret

The response is 202 Accepted with the report object, plus:

x-quota-remaining: 97

Create a suburb report

POST /api/v1/cobranded/suburb-reports
{
  "suburb": "Noosa Heads",
  "state": "QLD",
  "postcode": "4567",
  "regionId": 33,
  "externalReference": "crm-12346"
}
  • suburb, state and a 4-digit postcode are required.
  • state is one of NSW, VIC, QLD, WA, SA, TAS, NT or ACT.
  • Queensland requires regionId. Without it you’ll get 422 region_required.
  • For other states, regionId is optional and defaults to the state’s region.
  • regionId must belong to the state.

Queensland regions:

regionIdRegion
31Brisbane
32Gold Coast
33Sunshine Coast / Noosa
34Logan
35Moreton
36Ipswich
37Redland

Idempotency

Send an Idempotency-Key header with every create request. It’s optional but strongly recommended. Use a UUID, up to 128 characters; empty or longer values are ignored.

If the same key is sent again, the API returns the original report instead of creating a new one. It adds the header below and doesn’t use quota again:

idempotent-replay: true
  • Use one key for each logical submission, and reuse it when you retry after a timeout or a dropped connection.
  • Use a new key when someone deliberately tries again after a report has failed.

Poll for status

GET /api/v1/cobranded/reports/:id

This returns the report object. Responses are Cache-Control: no-store.

  • Poll every 2 to 5 seconds while the report is queued or generating.
  • Back off to every 10 seconds after about a minute.
  • Stop when the status is ready or failed.
async function waitForReport(id, { signal } = {}) {
  const started = Date.now();
  for (;;) {
    const res = await fetch(`https://api.mapview.ai/api/v1/cobranded/reports/${id}`, {
      headers: { Authorization: `Bearer ${process.env.MAPVIEW_PARTNER_KEY}` },
      signal,
    });
    if (!res.ok) throw new Error(`poll ${res.status}`);
    const report = await res.json();
    if (report.status === 'ready' || report.status === 'failed') return report;
    const wait = Date.now() - started < 60_000 ? 3_000 : 10_000;
    await new Promise((r) => setTimeout(r, wait));
  }
}

If you receive webhooks, use them as your main signal and keep polling as the fallback.

List reports

GET /api/v1/cobranded/reports?externalReference=crm-12345&limit=50
  • externalReference is optional and filters by your own ID.
  • limit defaults to 50, with a maximum of 200.
  • Results come newest first. There’s no cursor pagination yet.
  • externalReference isn’t unique: a retried job can produce more than one report with the same reference.

Use this to rebuild a report history screen after a reload, without storing report IDs yourself.

Download the PDF

GET /api/v1/cobranded/reports/:id/pdf

This returns application/pdf with Content-Disposition: attachment.

StatusErrorMeaning
409not_readyThe report hasn’t finished. Keep polling
404not_foundNo such report for your organisation
502pdf_unavailableThe stored PDF couldn’t be fetched. Try again

This endpoint needs your key, so a web page should download through your backend. To show the PDF in the browser, your backend can stream it with an inline disposition:

// Your backend (Node + Express): GET /api/reports/:id/pdf
import { Readable } from 'node:stream';

const upstream = await fetch(`https://api.mapview.ai/api/v1/cobranded/reports/${id}/pdf`, {
  headers: { Authorization: `Bearer ${process.env.MAPVIEW_PARTNER_KEY}` },
});
if (!upstream.ok) return res.status(upstream.status).json(await upstream.json());
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${reference}.pdf"`);
Readable.fromWeb(upstream.body).pipe(res);

Alternatively, the page can fetch the PDF from your backend and preview it as a blob:

const res = await fetch(`/api/reports/${id}/pdf`, { credentials: 'include' });
if (!res.ok) throw new Error(`PDF ${res.status}`);
iframe.src = URL.createObjectURL(await res.blob());

Don’t point an iframe at a URL that needs your partner key.

Usage and quota

GET /api/v1/cobranded/usage
{
  "periodStart": "2026-10-01",
  "monthlyQuota": 100,
  "used": 7,
  "remaining": 93,
  "addonActive": true
}
  • The quota period is a calendar month in Brisbane time (Australia/Brisbane). periodStart is the first day of the period.
  • Failed reports are refunded automatically.
  • Idempotent replays and validation errors don’t use quota.
  • When the quota runs out, create requests return 429 quota_exceeded with these headers:
Retry-After: 3600
x-quota-remaining: 0

Webhooks

Webhooks tell your server when a report finishes, so you don’t need to poll. They’re optional. Polling always works as a fallback.

MapView sets up a webhook URL for your organisation and gives you a signing secret (whsec_...) once. Your URL must use HTTPS. URLs that contain credentials, or that point at private or local hosts, are rejected. You can also send a callbackUrl with an individual report. That changes where the event goes, but it’s signed with the same secret. If your organisation has no webhook secret, no signed events are sent.

Events

report.ready and report.failed.

{
  "id": "377400d8-a07f-4cad-ad55-1b3328156bda",
  "event": "report.ready",
  "status": "ready",
  "reportType": 2,
  "externalReference": "crm-12345",
  "reportUrl": "https://reports.mapview.ai/cobranded/1/377400d8-a07f-4cad-ad55-1b3328156bda.pdf",
  "brandingApplied": true,
  "error": null,
  "createdAt": "2026-10-21T05:15:36.048Z"
}

Headers

x-mapview-event: report.ready
x-mapview-delivery: 6eb30a5e-686c-4984-85d2-3e9e65fbc8de
x-mapview-signature: t=1792559736,v1=<64 hex characters>
user-agent: mapview-cobranded-webhooks/1.0
content-type: application/json

Verify the signature

The signature is an HMAC-SHA256 of <timestamp>.<raw request body>, made with your whsec_... secret. Verify it against the raw bytes of the request. Don’t parse the body and serialise it again first. Reject events whose timestamp is more than five minutes old.

const crypto = require('crypto');

function verifyMapviewWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.`).update(rawBody).digest('hex');
  const a = Buffer.from(parts.v1 || '', 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib, hmac, time

def verify_mapview_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Delivery

  • Answer quickly. Return any 2xx response, then do the work from a queue. Any other response is retried.
  • Retries. There are six attempts, at roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours.
  • Duplicates. Deliveries are at least once, so the same event can arrive more than once. Ignore any event whose x-mapview-delivery ID you’ve already processed.

Delivery history and replay

GET  /api/v1/cobranded/reports/:id/deliveries
POST /api/v1/cobranded/reports/:id/webhook/replay

The delivery history lists each attempt with these fields:

  • event and status
  • attempt
  • status_code and response_ms
  • error
  • next_attempt_at, delivered_at and created_at.

Replay resets the report’s deliveries and returns { "replayed": <count> }.

Errors

Errors have this shape:

{
  "error": "postcode_mismatch",
  "message": "no G-NAF result in VIC 3004",
  "candidates": ["..."]
}
HTTPerrorWhat to do
400invalid_requestSomething required is missing. Fix the request
400invalid_callback_urlUse an HTTPS URL, or leave callbackUrl out
401unauthorizedThe key is missing, invalid or revoked. This is a configuration problem, so don’t ask your user to sign in again
403organisation_inactiveYour organisation is suspended or closed. Contact MapView
403addon_inactiveCo-branded reports aren’t turned on for your organisation. Contact MapView
403insufficient_scopeThe key lacks a scope. Contact MapView
409not_readyThe report is still being made. Keep polling
422unit_conflictThe unit in the address doesn’t match the unit field
422multiple_unitsAsk the user to choose one unit
422multiple_addressesAsk the user to choose one address
422unsupported_address_formatPO Box, LEVEL, LOT, REAR and OFF addresses aren’t supported
422invalid_addressThe address is empty or invalid
422not_foundThere’s no G-NAF match. Let the user edit the address
422postcode_mismatchShow candidates and let the user pick one
422ambiguous_addressShow candidates
422street_only_matchNo street-number match. Show candidates
422region_mismatchThe region doesn’t belong to the state
422unsupported_stateThe state isn’t supported
422region_requiredA Queensland suburb needs a regionId
429quota_exceededThis month’s quota is used up. See Retry-After
502address_resolution_failedTemporary. Try again
502pdf_unavailableTemporary or a storage problem. Keep the report ID and try again
503auth_unavailableTemporary. Try again

Branding

MapView sets up your branding during onboarding. There’s no self-service branding API yet. To make a change, send us the new details, then create a test report to see the result.

ItemDetails
LogoPNG, JPEG or WebP, up to 2 MB. No SVG. A transparent PNG at least 400 px tall, in a horizontal lockup, gives the best result. It’s normalised to a PNG up to 400 px tall
Display nameUp to 80 characters. Defaults to your organisation’s name
Footer straplineFree text, cut off at 160 characters, e.g. Acme Realty · 1300 555 010
Contact personName (required, up to 80 characters), title (up to 80), phone (up to 40, at least 6 digits) and email (up to 160). Optional
PlacementCover, interior page headers and footers. All three are recommended

We place a white chip behind dark logos on the navy cover and header, and a navy chip behind light logos in the white footer, but a transparent logo still looks best.

What the PDF looks like

These pages come from sample reports made for Acme Realty, a demo partner. Full samples are available from the co-branded reports page.

Sample property report cover: an aerial photo with the address, a navy band with the MapView logo on the left and the Acme Realty logo on the right, and the contact person in the bottom bar.
Property report cover
Sample interior page: a navy header with the page title, the address and the Acme Realty logo, and a footer with the partner logo, strapline and contact line.
Interior page, with the branded header and footer
Sample suburb report cover for Wantirna VIC, with both logos on the navy band and the contact person in the bottom bar.
Suburb report cover

The cover

  • Aerial imagery of the property or suburb fills the top, with the address or suburb name over it.
  • The report name is in a teal label in the top corner, for example “Premium Property Report” or “Suburb Report”.
  • Below the imagery, a navy band carries the MapView logo on the left and your logo on the right.
  • The left of the bottom bar reads “Prepared exclusively for …” with the date and mapview.ai.
  • The right of the bottom bar shows your contact person and strapline, in up to three lines:
Jane Smith · Senior Sales Consultant
0400 111 222 · jane@acme.example
Acme Realty · 1300 555 010

Interior pages

  • A navy header carries the page title, the address and your logo.
  • The footer carries:
    • your logo
    • “Generated by mapview.ai”
    • your strapline
    • one contact line: Contact: Jane Smith · Senior Sales Consultant · 0400 111 222 · jane@acme.example
    • the address, MapView’s terms and the page number.
  • The full-page section dividers, such as “01 Site Information”, carry MapView’s footer only.

Length. The sample property report runs to 53 pages and the suburb report to 18. The length varies with how much data there is for the address or suburb.

If your branding can’t be applied, the report is still produced as a standard MapView report, with brandingApplied: false.

Building a user interface

These flows suit a CRM screen or an internal tool.

Property report

  1. The user enters an address, and a unit if there is one.
  2. Your backend calls resolve.
  3. On a 422 with candidates, show a pick list and send the chosen candidate back through resolve.
  4. Show the canonical address and unit so the user can confirm them.
  5. Create the report with a new Idempotency-Key.
  6. Show progress while the report is queued or generating, and poll with backoff.
  7. When it’s ready, offer View and Download through your backend. Label reports with brandingApplied: false as “Standard MapView report”.
  8. When it has failed, show error.message and the report ID, and offer a retry with a new key.

Suburb report

Ask for the suburb, state and postcode. Require a region when the state is QLD. Then follow steps 5 to 8 above.

After a reload

Use GET /reports?externalReference=... and GET /reports/:id. Don’t rely on webhooks in the browser.

Usage

Show used / monthlyQuota and refresh it after each report. On a 429, explain that the quota is used up and use Retry-After to say when it resets.

Checklist

  • The mv_live_ key exists only in your server’s secret storage.
  • Addresses are resolved and confirmed before reports are created.
  • Every 422 with candidates is handled without losing what the user typed.
  • Queensland suburb reports ask for a region.
  • Each submission has its own Idempotency-Key, and deliberate retries get a new one.
  • Polling backs off and stops at ready or failed.
  • PDFs are fetched through your backend.
  • brandingApplied: false is shown as a standard report, not an error.
  • The quota and 429 states are handled.
  • Webhook signatures are verified against the raw body, and duplicates are ignored.

Support

When you report an integration problem, include:

  • the report ID or externalReference
  • the reportType and the request body (address or suburb, and region)
  • the HTTP status, and the error and message from the response
  • the brandingApplied value
  • whether the PDF came from the PDF endpoint or from reportUrl
  • for a webhook problem, the x-mapview-delivery ID and event.

Never send your API key or webhook secret to anyone, including MapView support.

Contact: hello@mapview.ai