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 POST with Content-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 OPTIONS is 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)

POST/api/public/extension/startno auth

Begins 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.

POST/api/public/extension/polldevice_code

Poll 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

POST/api/public/extension/entitlementsession_token

Call 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

reasonentitledExtension behaviour
trialingtrueFull functionality; show days left AND trial.unique_listings_remaining in the popup.
trial_listing_limit_reachedfalseTrial's 10 unique listings are used. Block Post to Marketplace and prompt to subscribe.
activetrueFull functionality.
past_due_gracetrueAllow work but show a prominent warning with a link to billing.
past_due_expiredfalseBlock and prompt to fix payment.
canceled_paid_throughtrueAllow until expires_at; show that access ends on that date.
trial_expiredfalseBlock and prompt to subscribe.
canceledfalseBlock and prompt to resubscribe.
inactivefalseBlock. Also returned when the device has been revoked (device.revoked = true).

3. Heartbeat

POST/api/public/extension/heartbeataccess_token

Call 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

POST/api/public/extension/usageaccess_token

Call 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

POST/api/public/extension/unlinksession_token

Extension-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

  1. Never rely on a cached entitled: true older than 24 hours. Re-check entitlement before each run; if the network fails, allow a cached grant for at most 24 hours, then block.
  2. If entitled is false, disable the Post to Marketplace action (keep it visible but inert), show the reason-specific message, and link to the relevant page: billing for past_due_*, pricing for trial_expired / trial_listing_limit_reached / canceled / inactive, and the devices page when device.revoked is true.
  3. On 401 invalid_session, clear the stored session token and restart the pairing flow.
  4. On 401 invalid_token, call the entitlement endpoint again to mint a new access token, then retry once.
  5. 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.