Extension API contract (v1.3.0)
The MarketBridge control plane is authoritative for entitlement. The extension never holds a permanent API key: it pairs once with a signed-in account, stores an opaque session token, and exchanges it for short-lived access tokens.
Base URL and conventions
- Base URL: the MarketBridge deployment origin, e.g.
https://app.marketbridge.example. All endpoints are under/api/public/extension/. - All requests are
POSTwithContent-Type: application/json. - Tokens may be sent either in the JSON body or as
Authorization: Bearer <token>. Bearer is preferred. - CORS is open for extension origins; preflight
OPTIONSis handled. - Errors use HTTP status plus
{ "error": { "code": "...", "message": "..." } }.
Device identity
On first run the extension generates a random device_public_id (recommended: 32 hex characters from crypto.getRandomValues) and stores it in chrome.storage.local. It is an opaque identifier — never a licence key. It must be stable across browser restarts and must not be shared between profiles. Each account may have at most 2 non-revoked devices.
1. Pairing (one time per browser)
/api/public/extension/startno authBegins pairing and returns a user-facing code plus the URL where the agent approves it.
POST /api/public/extension/start
{
"device_public_id": "9f2c…",
"friendly_name": "Chrome on MacBook Pro",
"extension_version": "1.3.0"
}
200 OK
{
"device_code": "kR9…", // secret, keep in extension storage
"user_code": "K7F2-9QLD", // show to the user
"verification_url": "https://app.example/link",
"verification_url_complete": "https://app.example/link?code=K7F2-9QLD",
"expires_in": 600,
"interval": 5
}Open verification_url_complete in a new tab. The user signs in (or signs up) and approves the browser.
/api/public/extension/polldevice_codePoll every interval seconds until approval. 202 means still pending. The session token is returned exactly once.
POST /api/public/extension/poll
{ "device_code": "kR9…" }
202 Accepted { "status": "pending" }
403 Forbidden { "error": { "code": "access_denied", … } }
400 Bad Request { "error": { "code": "expired_token", … } }
200 OK
{
"status": "approved",
"session_token": "…", // store securely, long-lived (90 days)
"session_expires_at": "2026-11-19T09:00:00.000Z",
"access_token": "mb1.…", // short-lived (15 min)
"access_token_expires_at": "2026-08-21T07:20:00.000Z",
"entitlement": { … } // same shape as below
}2. Entitlement check
/api/public/extension/entitlementsession_tokenCall on browser startup, when the popup opens, and before starting a Post to Marketplace run. Returns a fresh access_token whenever entitled.
POST /api/public/extension/entitlement
Authorization: Bearer <session_token>
200 OK
{
"entitled": true,
"status": "trialing", // trialing | active | past_due | canceled | inactive
"plan": "founding_agent",
"plan_name": "Founding Agent",
"reason": "trialing",
"expires_at": "2026-08-28T07:05:00.000Z",
"cancel_at_period_end": false,
"device": { "id": "…", "revoked": false, "active_count": 1, "limit": 2 },
"usage": { "billing_period_key": "trial:2026-08-28", "listings_processed": 4 },
"trial": {
"usage_limited": true, // true only while status = trialing
"unique_listing_limit": 10, // null on paid plans (no cap)
"unique_listings_used": 4,
"unique_listings_remaining": 6
},
"access_token": "mb1.…",
"access_token_expires_at": "2026-08-21T07:20:00.000Z"
}
401 Unauthorized
{ "error": { "code": "invalid_session", "message": "Session expired or revoked. Re-link this browser." } }Entitlement states
| reason | entitled | Extension behaviour |
|---|---|---|
trialing | true | Full functionality; show days left AND trial.unique_listings_remaining in the popup. |
trial_listing_limit_reached | false | Trial's 10 unique listings are used. Block Post to Marketplace and prompt to subscribe. |
active | true | Full functionality. |
past_due_grace | true | Allow work but show a prominent warning with a link to billing. |
past_due_expired | false | Block and prompt to fix payment. |
canceled_paid_through | true | Allow until expires_at; show that access ends on that date. |
trial_expired | false | Block and prompt to subscribe. |
canceled | false | Block and prompt to resubscribe. |
inactive | false | Block. Also returned when the device has been revoked (device.revoked = true). |
3. Heartbeat
/api/public/extension/heartbeataccess_tokenCall at most once per hour to refresh last_seen_at and optionally update the friendly name / version. Returns the current entitlement payload.
POST /api/public/extension/heartbeat
Authorization: Bearer <access_token>
{ "friendly_name": "Chrome on MacBook Pro", "extension_version": "1.3.0" }
200 OK { "entitled": true, … }
401 { "error": { "code": "invalid_token", "message": "Access token expired. Refresh it." } }4. Record a processed listing
/api/public/extension/usageaccess_tokenCall once a listing has been successfully prepared. The same source + source_listing_id pair inside the same billing period (or the same trial) is idempotent — it is recorded once and later calls return duplicate: true. Retrying or reprocessing a property therefore never consumes another trial listing.
POST /api/public/extension/usage
Authorization: Bearer <access_token>
{
"source": "zillow", // zillow | manual | other
"source_listing_id": "2078123456", // ZPID or other source ID
"address": "1424 Juniper Ave, Austin, TX 78702",
"status": "processed" // processed | failed
}
200 OK
{
"recorded": true,
"duplicate": false,
"listing_id": "…",
"billing_period_key": "period:2026-09-21",
"entitlement": { … }
}
403 { "error": { "code": "not_entitled", … } }
403 { "error": { "code": "trial_limit_reached", "message": "Free trial limit of 10 unique listings reached. Subscribe to continue." } }5. Unlink this browser
/api/public/extension/unlinksession_tokenExtension-side sign-out. Revokes the session but keeps the device record, so the seat can be re-activated later. Always responds { "unlinked": true }.
Post to Marketplace when access is denied
- Never rely on a cached
entitled: trueolder than 24 hours. Re-check entitlement before each run; if the network fails, allow a cached grant for at most 24 hours, then block. - If
entitledis false, disable the Post to Marketplace action (keep it visible but inert), show the reason-specific message, and link to the relevant page: billing forpast_due_*, pricing fortrial_expired/trial_listing_limit_reached/canceled/inactive, and the devices page whendevice.revokedis true. - On
401 invalid_session, clear the stored session token and restart the pairing flow. - On
401 invalid_token, call the entitlement endpoint again to mint a new access token, then retry once. - Never write the outcome of a blocked run to the usage endpoint, and never surface raw server errors to the agent.
Security expectations
- Session tokens are stored hashed server-side; they are shown to the extension once.
- Access tokens are HMAC-signed, expire in 15 minutes, and are bound to one device and session.
- Revoking a browser in the dashboard revokes its sessions immediately.
- A lapsed subscription revokes extension sessions once the paid-through date passes.
- There is no shared or copyable customer API key anywhere in this design.