> ## Documentation Index
> Fetch the complete documentation index at: https://developers.investorlift.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart for buyers

> Search the deals you see on Mogul, read one, preview an offer, submit it with the preview token and follow the seller's counter.

<Note>
  The Marketplace API is in a private beta. It answers only for accounts Investorlift has enabled. [Access](/marketplace/access) says how to ask for one. A route shape on these pages can change before the beta ends. The changelog records every change.
</Note>

On this page you act as a buyer. You search the deals Mogul shows you, read one, preview an offer and submit it. Then you
answer the seller's counter. Every request goes to `https://api.investorlift.com/marketplace/v1` with an OAuth token.

The token needs four scopes. `marketplace:profile` is for the check in step 1. `deals:read` is for the search and the
deal read. `offers:write` is for the preview, the submit and the counter, and `offers:read` is for the offer read. The
optional step 7 adds `webhooks:manage` or `events:read`.

<Steps>
  <Step title="Get access and a token">
    [Access](/marketplace/access) says how Investorlift enables your account. [Authentication](/marketplace/authentication)
    says how your client gets an access token for the audience `https://api.investorlift.com/marketplace/v1`.

    Put the token in your shell. Every snippet on this page reads it from `MARKETPLACE_TOKEN`.

    ```bash theme={null}
    export MARKETPLACE_TOKEN="eyJ..."
    ```

    Check what the token can do. `GET /me` needs `marketplace:profile`.

    <CodeGroup>
      ```bash curl theme={null}
      curl "https://api.investorlift.com/marketplace/v1/me" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN"
      ```

      ```javascript Node.js theme={null}
      const res = await fetch("https://api.investorlift.com/marketplace/v1/me", {
        headers: { Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}` },
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const body = await res.json();
      ```

      ```python Python theme={null}
      import os

      import requests

      r = requests.get(
          "https://api.investorlift.com/marketplace/v1/me",
          headers={"Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}"},
          timeout=30,
      )
      r.raise_for_status()
      body = r.json()
      ```
    </CodeGroup>

    The block is the answer, cut to the fields this step explains.

    ```json theme={null}
    {
      "data": {
        "user": { "id": "byr_6Pd2kM4vTq8n" },
        "designations": ["buyer"],
        "sides": ["buy"],
        "scopes": ["deals:read", "marketplace:profile", "offers:read", "offers:write"],
        "trust": {
          "tier": "T2",
          "next_step": "Get one offer accepted on Investorlift."
        },
        "terms": {
          "sell": { "accepted": false, "agreement_id": "marketplace-api-terms-v2026-09-18" },
          "buy": { "accepted": true, "agreement_id": "marketplace-api-terms-v2026-09-18" }
        },
        "environment": "production",
        "api_version": "2026-09-18"
      },
      "meta": {
        "request_id": "6f1c2a8e-3b7d-4f21-9a0c-2e5b81d7a4f3",
        "api_version": "2026-09-18"
      }
    }
    ```

    `sides` must carry `buy`. `trust.tier` is your trust tier, and `trust.next_step` says what raises it. The full answer
    also carries `caps`: one row per daily cap, with `limit`, `used`, `remaining` and `resets_at`. The `deal_read` row is your
    deal budget. [Trust and limits](/marketplace/concepts/trust-and-limits) explains the tiers.
  </Step>

  <Step title="Search deals">
    The search reads the deals you see on Mogul, under the same visibility rule, 25 to a page. Filter by price and property
    type.

    <CodeGroup>
      ```bash curl theme={null}
      curl "https://api.investorlift.com/marketplace/v1/buy/deals?price_max=300000&type=single_family" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN"
      ```

      ```javascript Node.js theme={null}
      const params = new URLSearchParams({ price_max: "300000", type: "single_family" });
      const res = await fetch(`https://api.investorlift.com/marketplace/v1/buy/deals?${params}`, {
        headers: { Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}` },
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const { data: deals, page } = await res.json();
      ```

      ```python Python theme={null}
      r = requests.get(
          "https://api.investorlift.com/marketplace/v1/buy/deals",
          params={"price_max": 300000, "type": "single_family"},
          headers={"Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}"},
          timeout=30,
      )
      r.raise_for_status()
      deals, page = r.json()["data"], r.json()["page"]
      ```
    </CodeGroup>

    The answer, cut to one row:

    ```json theme={null}
    {
      "data": [
        {
          "id": "mdl_5Rt7yU1pKm3e",
          "status": "available",
          "asking_price": 285000,
          "arv_estimate": 410000,
          "min_emd": 5000,
          "condition": "MAJOR_REHAB",
          "address": {
            "status": "hidden",
            "city": "Phoenix",
            "state": "AZ",
            "zip": "85018",
            "is_approximate": true
          },
          "property": { "type": "single_family", "beds": 3, "baths": 2, "sq_footage": 1640, "year_built": 1978 },
          "seller": { "id": "slr_3Kd9mQ2xB7Lp", "name": "SAGUARO HOLDINGS LLC", "rating": 4.8, "review_count": 37 },
          "published_at": "2026-09-18T15:14:02Z",
          "expires_at": "2026-10-18T15:14:02Z"
        }
      ],
      "page": {
        "next_cursor": "c1.25.Qm9ndXNTaWduYXR1cmVGb3JEb2Nz",
        "limit": 25,
        "returned": 25
      },
      "meta": {
        "request_id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
        "api_version": "2026-09-18"
      }
    }
    ```

    Every row counts against your daily deal budget, and one deal counts once a day. Read what you need, and follow
    `page.next_cursor` until it is null.
    The street stays hidden until the seller shares it. While `address.is_approximate` is true, the coordinates sit on a
    coarse grid. A buyer with no attached or verified legal entity reads the first 50 deals only. `trust.next_step` in
    `GET /me` names the entity step.
  </Step>

  <Step title="Read one deal">
    One read counts one row against the deal budget. It adds the media, the documents, the verification state and your own
    offers and requests on the deal.

    <CodeGroup>
      ```bash curl theme={null}
      curl "https://api.investorlift.com/marketplace/v1/buy/deals/mdl_5Rt7yU1pKm3e" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN"
      ```

      ```javascript Node.js theme={null}
      const res = await fetch("https://api.investorlift.com/marketplace/v1/buy/deals/mdl_5Rt7yU1pKm3e", {
        headers: { Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}` },
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const deal = (await res.json()).data;
      ```

      ```python Python theme={null}
      r = requests.get(
          "https://api.investorlift.com/marketplace/v1/buy/deals/mdl_5Rt7yU1pKm3e",
          headers={"Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}"},
          timeout=30,
      )
      r.raise_for_status()
      deal = r.json()["data"]
      ```
    </CodeGroup>

    The answer, cut to the address block, the seller stub and your own actions:

    ```json theme={null}
    {
      "data": {
        "id": "mdl_5Rt7yU1pKm3e",
        "status": "available",
        "asking_price": 285000,
        "min_emd": 5000,
        "address": {
          "status": "hidden",
          "city": "Phoenix",
          "state": "AZ",
          "zip": "85018",
          "is_approximate": true,
          "agreement": {
            "id": "non-circumvention-v1",
            "url": "https://mogul.investorlift.com/api/agreements?agreementId=non-circumvention-v1&contextEntityId=5Rt7yU1pKm3e",
            "accepted": false
          }
        },
        "seller": {
          "id": "slr_3Kd9mQ2xB7Lp",
          "name": "SAGUARO HOLDINGS LLC",
          "rating": 4.8,
          "review_count": 37,
          "phone": null
        },
        "my_offers": [],
        "my_requests": []
      },
      "meta": {
        "request_id": "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a",
        "api_version": "2026-09-18"
      }
    }
    ```

    `address.status` reads `hidden`, and `street` is absent, not null, until the seller shares it. An address request
    through `POST /buy/deals/{deal_id}/inquiries` asks for it. The status then reads `requested`. It reads `approved` once
    the seller shares the street, and `declined` after the seller declines. `my_requests[].address_state` carries the same
    state for each of your address requests, and it is null on a plain inquiry.

    `address.agreement` names the Non-Circumvention Agreement that the request binds to. `url` is where a person reads it,
    and `accepted` says if your organization accepted it. Show the agreement before the person accepts. Without the
    acceptance, the request answers `409 agreement_required`, and the body carries the same `agreement: { id, url }`. Send
    `accept_agreement: { "id": "<agreement.id>" }` with the request.

    A new inquiry or address request answers `201`. When the request exists already, the API answers `200` with the
    existing row and writes nothing. When the hold queue takes the request, the answer is `202`.

    `seller.phone` is the Investorlift tracking number of the deal manager, or null. The seller's own phone and email never
    appear. [What the API returns](/marketplace/concepts/what-the-api-returns) has the rest of the object.
  </Step>

  <Step title="Preview an offer">
    An offer is a two-step write. The preview measures your terms against the deal and the seller's requirements, and writes
    nothing. Send the amount, the earnest money deposit, the financing and a note of at most 500 characters.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST "https://api.investorlift.com/marketplace/v1/buy/deals/mdl_5Rt7yU1pKm3e/offers/preview" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{ "offer_amount": 270000, "emd_amount": 5000, "financing": "cash", "note": "Cash, close in 14 days, no inspection contingency." }'
      ```

      ```javascript Node.js theme={null}
      const terms = {
        offer_amount: 270000,
        emd_amount: 5000,
        financing: "cash",
        note: "Cash, close in 14 days, no inspection contingency.",
      };
      const res = await fetch("https://api.investorlift.com/marketplace/v1/buy/deals/mdl_5Rt7yU1pKm3e/offers/preview", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(terms),
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const preview = (await res.json()).data;
      ```

      ```python Python theme={null}
      terms = {
          "offer_amount": 270000,
          "emd_amount": 5000,
          "financing": "cash",
          "note": "Cash, close in 14 days, no inspection contingency.",
      }
      r = requests.post(
          "https://api.investorlift.com/marketplace/v1/buy/deals/mdl_5Rt7yU1pKm3e/offers/preview",
          json=terms,
          headers={"Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}"},
          timeout=30,
      )
      r.raise_for_status()
      preview = r.json()["data"]
      ```
    </CodeGroup>

    The answer, cut to the deal stamp, four requirements and the token:

    ```json theme={null}
    {
      "data": {
        "deal": { "id": "mdl_5Rt7yU1pKm3e", "status": "available", "asking_price": 285000, "min_emd": 5000 },
        "terms": {
          "offer_amount": 270000,
          "emd_amount": 5000,
          "financing": "cash",
          "note": "Cash, close in 14 days, no inspection contingency."
        },
        "requirements": [
          { "code": "deal_offerable", "met": true, "blocking": true },
          { "code": "proof_of_funds_current", "met": true, "blocking": true },
          { "code": "min_emd", "met": true, "blocking": true },
          { "code": "above_floor", "met": true, "blocking": true }
        ],
        "can_submit": true,
        "open_offer_id": null,
        "standing": { "tier": "T2" },
        "preview_token": "ZGVhbDptZGxfNVJ0N3lVMXBLbTNlOjE3NTgyMTQyNjc.Qm9ndXNTaWduYXR1cmVGb3JEb2Nz",
        "expires_at": "2026-09-18T16:21:07Z"
      },
      "meta": {
        "request_id": "5e6f7a8b-9c0d-4e1f-2a3b-4c5d6e7f8a9b",
        "api_version": "2026-09-18"
      }
    }
    ```

    Each requirement is a code with `met` and `blocking`, and no figure the seller keeps private. `can_submit` is true when
    your terms meet every blocking requirement. `standing` carries your tier, the offers and previews left today, and your open chains.
    The `preview_token` proves you saw these terms and lives 5 minutes. A preview counts against your previews cap, and once
    a day per deal against the deal budget.

    If `proof_of_funds_current` is unmet, upload a document in three hops. Ask for a ticket at
    `POST /buy/me/proof-of-funds/uploads` with `filename`, `content_type` and `size_bytes`. Post the file to the ticket's
    `url` with its `fields`. The ticket names the largest file in `max_bytes`, and its life in seconds in `expires_in`.
    Then finish at `POST /buy/me/proof-of-funds` with `key`, `filename`, `content_type`, `size_bytes` and an
    `Idempotency-Key`.

    The [seller quickstart](/marketplace/quickstart-seller) shows the same three hops for a photo. Then preview again.
  </Step>

  <Step title="Submit the offer">
    The submit sends the same terms and the `preview_token`. It needs an `Idempotency-Key`, a string of your own that the API
    keeps for 24 hours. The API runs every check the preview ran and refuses a deal that moved in between.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST "https://api.investorlift.com/marketplace/v1/buy/offers" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: $(uuidgen)" \
        -d '{
          "deal_id": "mdl_5Rt7yU1pKm3e",
          "offer_amount": 270000,
          "emd_amount": 5000,
          "financing": "cash",
          "note": "Cash, close in 14 days, no inspection contingency.",
          "preview_token": "ZGVhbDptZGxfNVJ0N3lVMXBLbTNlOjE3NTgyMTQyNjc.Qm9ndXNTaWduYXR1cmVGb3JEb2Nz"
        }'
      ```

      ```javascript Node.js theme={null}
      import crypto from "node:crypto";

      const res = await fetch("https://api.investorlift.com/marketplace/v1/buy/offers", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}`,
          "Content-Type": "application/json",
          "Idempotency-Key": crypto.randomUUID(),
        },
        body: JSON.stringify({ deal_id: "mdl_5Rt7yU1pKm3e", ...terms, preview_token: preview.preview_token }),
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const offer = (await res.json()).data;
      ```

      ```python Python theme={null}
      import uuid

      r = requests.post(
          "https://api.investorlift.com/marketplace/v1/buy/offers",
          json={"deal_id": "mdl_5Rt7yU1pKm3e", **terms, "preview_token": preview["preview_token"]},
          headers={
              "Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}",
              "Idempotency-Key": str(uuid.uuid4()),
          },
          timeout=30,
      )
      r.raise_for_status()
      offer = r.json()["data"]
      ```
    </CodeGroup>

    The answer is `201` with the first round of your chain, cut here to the fields this step explains. The id starts with
    `ofr_`. When the hold queue takes the offer, the answer is `202` with `status: held` and a `hold_id`, and no offer id.
    [Trust and limits](/marketplace/concepts/trust-and-limits#the-hold-queue) says what a held write is.

    ```json theme={null}
    {
      "data": {
        "id": "ofr_4Wq8zX2nJd6b",
        "deal_id": "mdl_5Rt7yU1pKm3e",
        "buyer_id": "byr_6Pd2kM4vTq8n",
        "status": "new",
        "offer_amount": 270000,
        "emd_amount": 5000,
        "financing": "cash",
        "submitted_by": "buyer",
        "round": 1,
        "parent_offer_id": null,
        "root_offer_id": null,
        "note": "Cash, close in 14 days, no inspection contingency.",
        "decline_reason": null,
        "channel": "api",
        "created_at": "2026-09-18T16:18:40Z",
        "updated_at": "2026-09-18T16:18:40Z",
        "chain": [
          { "id": "ofr_4Wq8zX2nJd6b", "round": 1, "status": "new", "submitted_by": "buyer", "offer_amount": 270000 }
        ]
      },
      "meta": {
        "request_id": "7a8b9c0d-1e2f-4a3b-4c5d-6e7f8a9b0c1d",
        "api_version": "2026-09-18"
      }
    }
    ```

    The seller gets the offer at once, in the app and on their webhooks. A repeat of the same key with the same body replays
    this answer with the header `Idempotent-Replayed: true`. Three refusals are common here.

    | Status and code       | Why                                                                                                               | What to do                                                                |
    | --------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
    | `409 deal_changed`    | The deal's status, price, minimum deposit or settings moved after the preview. The body carries the current terms | Read the current terms in the body, take a new preview, then submit again |
    | `409 offer_exists`    | One active chain per buyer per deal. The body names the open round                                                | Withdraw or finish your open offer on this deal, then make a new one      |
    | `422 preview_expired` | More than 5 minutes passed since the preview                                                                      | Take a new preview, then submit inside five minutes                       |

    [Errors](/marketplace/concepts/errors) lists every code, and [Idempotency](/marketplace/concepts/idempotency) has the two
    key conflicts.
  </Step>

  <Step title="Follow the counter">
    A seller's counter opens round 2 and marks your round `countered`. The event `offer.countered` tells you, or you read
    the offer.

    <CodeGroup>
      ```bash curl theme={null}
      curl "https://api.investorlift.com/marketplace/v1/buy/offers/ofr_4Wq8zX2nJd6b" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN"
      ```

      ```javascript Node.js theme={null}
      const res = await fetch("https://api.investorlift.com/marketplace/v1/buy/offers/ofr_4Wq8zX2nJd6b", {
        headers: { Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}` },
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const offer = (await res.json()).data;
      const openRound = offer.chain.at(-1);
      ```

      ```python Python theme={null}
      r = requests.get(
          "https://api.investorlift.com/marketplace/v1/buy/offers/ofr_4Wq8zX2nJd6b",
          headers={"Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}"},
          timeout=30,
      )
      r.raise_for_status()
      offer = r.json()["data"]
      open_round = offer["chain"][-1]
      ```
    </CodeGroup>

    The answer, cut to the chain:

    ```json theme={null}
    {
      "data": {
        "id": "ofr_4Wq8zX2nJd6b",
        "status": "countered",
        "round": 1,
        "chain": [
          { "id": "ofr_4Wq8zX2nJd6b", "round": 1, "status": "countered", "submitted_by": "buyer", "offer_amount": 270000, "emd_amount": 5000 },
          { "id": "ofr_9Bc5tY7mLs1f", "round": 2, "status": "new", "submitted_by": "seller", "offer_amount": 278000, "emd_amount": 7500, "parent_offer_id": "ofr_4Wq8zX2nJd6b" }
        ]
      },
      "meta": {
        "request_id": "8b9c0d1e-2f3a-4b4c-5d6e-7f8a9b0c1d2e",
        "api_version": "2026-09-18"
      }
    }
    ```

    The open round is the last one of the chain, and you act on its id. Sides alternate, so an action on your own round
    answers `409 wrong_side`. A round that is no longer open answers `409 offer_superseded` with the open round's id. You have
    three answers.

    A counter is a two-step write like the first offer. Preview it on the open round, then send the token. The counter
    needs the `preview_token`, as the first offer does.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST "https://api.investorlift.com/marketplace/v1/buy/offers/ofr_9Bc5tY7mLs1f/counter/preview" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{ "offer_amount": 274000, "emd_amount": 7500, "financing": "cash" }'

      curl -X POST "https://api.investorlift.com/marketplace/v1/buy/offers/ofr_9Bc5tY7mLs1f/counter" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: $(uuidgen)" \
        -d '{ "offer_amount": 274000, "emd_amount": 7500, "financing": "cash", "preview_token": "<preview_token from the answer above>" }'
      ```

      ```javascript Node.js theme={null}
      import crypto from "node:crypto";

      const base = "https://api.investorlift.com/marketplace/v1/buy/offers";
      const headers = {
        Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}`,
        "Content-Type": "application/json",
      };
      const counter = { offer_amount: 274000, emd_amount: 7500, financing: "cash" };

      const previewRes = await fetch(`${base}/${openRound.id}/counter/preview`, {
        method: "POST",
        headers,
        body: JSON.stringify(counter),
      });
      if (!previewRes.ok) throw new Error(`${previewRes.status} ${await previewRes.text()}`);
      const counterPreview = (await previewRes.json()).data;

      const res = await fetch(`${base}/${openRound.id}/counter`, {
        method: "POST",
        headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
        body: JSON.stringify({ ...counter, preview_token: counterPreview.preview_token }),
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const round3 = (await res.json()).data;
      ```

      ```python Python theme={null}
      import uuid

      base = "https://api.investorlift.com/marketplace/v1/buy/offers"
      headers = {"Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}"}
      counter = {"offer_amount": 274000, "emd_amount": 7500, "financing": "cash"}

      p = requests.post(f"{base}/{open_round['id']}/counter/preview", json=counter, headers=headers, timeout=30)
      p.raise_for_status()
      counter_preview = p.json()["data"]

      r = requests.post(
          f"{base}/{open_round['id']}/counter",
          json={**counter, "preview_token": counter_preview["preview_token"]},
          headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
          timeout=30,
      )
      r.raise_for_status()
      round3 = r.json()["data"]
      ```
    </CodeGroup>

    The counter answers `201` with the new round of your chain. When the hold queue takes the counter, the answer is
    `202` with a `hold_id`.

    The two other answers take one request each on the open round's id, with an `Idempotency-Key`.

    | Answer                    | Request                                                                                                     | Body                                                      |
    | ------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
    | Accept the seller's terms | `POST /buy/offers/{offer_id}/counter/preview` with an empty body, then `POST /buy/offers/{offer_id}/accept` | `{ "preview_token": "..." }`. The deal moves to `pending` |
    | Decline                   | `POST /buy/offers/{offer_id}/decline`                                                                       | `{ "reason": "price_too_low" }`. The chain closes         |

    An accept runs the two-step rule too, because it commits money. A counter counts toward your active chains, never toward
    the daily new-offer cap.
  </Step>

  <Step title="Get the events by webhook">
    This step is optional. A buy-side webhook endpoint gets `offer.countered` and `offer.accepted` as they occur, signed. It
    needs `webhooks:manage`, an `https` URL on a public host, and an `Idempotency-Key`.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST "https://api.investorlift.com/marketplace/v1/buy/webhooks" \
        -H "Authorization: Bearer $MARKETPLACE_TOKEN" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: $(uuidgen)" \
        -d '{ "url": "https://deals.example.com/investorlift/webhooks", "event_types": ["offer.countered", "offer.accepted"] }'
      ```

      ```javascript Node.js theme={null}
      import crypto from "node:crypto";

      const res = await fetch("https://api.investorlift.com/marketplace/v1/buy/webhooks", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.MARKETPLACE_TOKEN}`,
          "Content-Type": "application/json",
          "Idempotency-Key": crypto.randomUUID(),
        },
        body: JSON.stringify({
          url: "https://deals.example.com/investorlift/webhooks",
          event_types: ["offer.countered", "offer.accepted"],
        }),
      });
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      const endpoint = (await res.json()).data;
      ```

      ```python Python theme={null}
      import uuid

      r = requests.post(
          "https://api.investorlift.com/marketplace/v1/buy/webhooks",
          json={
              "url": "https://deals.example.com/investorlift/webhooks",
              "event_types": ["offer.countered", "offer.accepted"],
          },
          headers={
              "Authorization": f"Bearer {os.environ['MARKETPLACE_TOKEN']}",
              "Idempotency-Key": str(uuid.uuid4()),
          },
          timeout=30,
      )
      r.raise_for_status()
      endpoint = r.json()["data"]
      ```
    </CodeGroup>

    The answer shows the signing secret once, and the endpoint starts at `pending_verification`. The API sends a challenge to
    the URL, your endpoint echoes it, and the status becomes `active`. Verify every delivery with `webhook-id`,
    `webhook-timestamp` and `webhook-signature`. The event id is your dedupe key, because delivery is at least once and
    unordered.

    Without a webhook, `GET /buy/events?types=offer.countered,offer.accepted` reads the same events in sequence order with
    `events:read`. Follow `page.next_cursor` and keep the last cursor. The buy side also gets `offer.declined`,
    `address_request.approved` and `address_request.declined`. It gets `deal.matched` for a buy box, and
    `deal.status_changed` for a deal you hold an offer on. It gets `offer.held` and `inquiry.held` when the hold queue takes
    one of your writes. [Webhooks and events](/marketplace/concepts/webhooks-and-events) has the catalogue and the
    verification code.
  </Step>
</Steps>

## Where to go next

<CardGroup cols={3}>
  <Card title="Authentication" icon="id-badge" href="/marketplace/authentication">
    The OAuth flow at the Investorlift issuer, the scopes and the roles.
  </Card>

  <Card title="What the API returns" icon="brackets-curly" href="/marketplace/concepts/what-the-api-returns">
    The deal object a buyer reads, the hidden address, the seller stub, and what never appears.
  </Card>

  <Card title="Trust and limits" icon="gauge" href="/marketplace/concepts/trust-and-limits">
    The four trust tiers, the daily deal budget, the caps and how to raise your tier.
  </Card>

  <Card title="Idempotency" icon="list-check" href="/marketplace/concepts/idempotency">
    The key, the 24 hours, the replay header and the two conflicts.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/marketplace/concepts/errors">
    Every code the API answers, with a body to recognise and the recovery.
  </Card>

  <Card title="Quickstart for sellers" icon="house" href="/marketplace/quickstart-seller">
    The other side: draft, photo, contract, publish and webhook.
  </Card>
</CardGroup>


## Related topics

- [Quickstart for sellers](/marketplace/quickstart-seller.md)
- [The Marketplace API](/marketplace/overview.md)
- [Quickstart](/guides/quickstart.md)
- [Authentication](/marketplace/authentication.md)
- [Changelog](/changelog.md)
