Skip to content

For developers

API and webhooks

Read and write your brokerage’s listings, leads and appointments on The Yellow Page from your own systems, and be told when something changes.

The API and webhooks are available to brokerages whose plan includes them. Keys are created by an owner of the brokerage in their dashboard, on the “Developers” tab. See plans

Base URL
https://api.yellowecuador.com/api/public/v1

Authentication

Every request carries a key in the Authorization header, with the Bearer scheme. The key belongs to one brokerage: everything it reads or writes is that brokerage’s.

curl "https://api.yellowecuador.com/api/public/v1/me" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"
  • The key is shown once, when it is created. We keep only its fingerprint.
  • Use it from a server only. Do not put it in a web page or an app: whoever sees it can use it.
  • A revoked or expired key stops working on the next request.

Scopes

  • listings:readRead the listings.
  • listings:writeCreate, edit and publish listings; add photos.
  • leads:readRead the leads.
  • leads:writeSend leads in, move their stage and add notes.
  • appointments:readRead the appointments.
  • webhooks:manageCreate, change and delete webhook endpoints.

Conventions

  • Everything is JSON, in UTF-8. Dates are ISO 8601 instants in UTC.
  • An id that is not your brokerage’s answers 404, exactly like one that does not exist.
  • Lists return “data” and “nextCursor”. Ask for the next page with cursor=nextCursor; when it is null there is no more. The limit parameter goes from 1 to 100 (25 when you send none).
  • Your brokerage may make 120 requests a minute in all — every key counts toward the same number, and one key may use at most half of it (at least 30) so it cannot starve the others — unless your contract says another number. The RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers tell you how many are left; past the limit the answer is 429 with Retry-After.
  • Listings created through the API have their own budget, apart from the owner's in the dashboard: 600 an hour per brokerage (more if your contract raises the rate) and at most 500 drafts made through the API (2000 with bulk import). Past them the answer is 429, or 409 too_many_drafts.
  • Any POST may carry the Idempotency-Key header. If you repeat the request with the same key and the same body within 24 hours, you get the original answer and nothing is created twice.
  • An unknown field in the body or the query is a 400 error: nothing is ignored in silence.
  • The version is in the path. Inside v1 we only add: new fields, new routes, new events.

Errors

An error arrives as application/problem+json. The last segment of “type” is the code in the table; “detail” explains it in English and, for validation errors, “errors” lists every field.

{
  "type": "https://casa-ec.dev/problems/insufficient_scope",
  "title": "Forbidden",
  "status": 403,
  "detail": "This key does not hold the \"leads:write\" scope",
  "scope": "leads:write",
  "requestId": "…"
}
  • 401invalid_api_keyThe key is missing, malformed, revoked, expired, or its brokerage no longer has API access.
  • 403insufficient_scopeThe key does not hold the scope this route needs (see `scope` in the body).
  • 403no_acting_ownerThe brokerage has no active owner the key could act for.
  • 404not_foundNo such resource in this brokerage.
  • 400validationThe body or the query is not valid; `errors` lists every field.
  • 400invalid_cursorThe cursor does not belong to this list.
  • 429rate_limitedToo many requests (per brokerage and per key, or the hourly budget of listing creations); wait `Retry-After` seconds.
  • 409too_many_draftsThe brokerage holds 500 drafts made through the API (2 000 with bulk import); publish or delete some first.
  • 402plan_requiredThe plan does not include this feature.
  • 402plan_limit_reachedA limit of the plan was reached (active listings).
  • 409invalid_transitionThe listing cannot go from its status to the one asked.
  • 409version_conflictThe lead changed since `version` was read.
  • 409duplicate_contactReopening the lead would duplicate an open one.
  • 409idempotency_in_progressA request with this Idempotency-Key is still running.
  • 422idempotency_key_reusedThis Idempotency-Key was used with a different request.
  • 422listing_incompleteThe listing lacks what publishing requires; `errors` lists it.
  • 422unknown_geoUnknown or inconsistent province / canton / parish.
  • 422not_a_member`ownerId` is not an active professional member of the brokerage.
  • 422unsafe_urlThe URL is not an https address on the public internet.
  • 422fetch_failedThe image could not be downloaded.
  • 422not_an_imageThe file is not a JPEG, PNG or WebP image.
  • 422too_largeThe image is larger than 10 MB.
  • 422media_limitA listing holds at most 30 images.
  • 503outbound_busyOur outbound capacity is momentarily full; retry after `Retry-After` seconds.
  • 409endpoint_limitA brokerage holds at most 10 webhook endpoints.

The key and the document

Check a key, and download this same reference as an OpenAPI document.

GET/me

The key in use, its scopes and its rate limit

Any valid key.

curl

curl "https://api.yellowecuador.com/api/public/v1/me" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/me", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/openapi.json

This API as an OpenAPI 3.1 document

Needs no key.

curl

curl "https://api.yellowecuador.com/api/public/v1/openapi.json"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/openapi.json", {
});
const data = await res.json();

Answers 200 when all is well.

Reference data

The codes listings use: provinces, cantons and parishes (INEC), property types and features.

GET/reference/provinces

Provinces (INEC codes)

Any valid key.

curl

curl "https://api.yellowecuador.com/api/public/v1/reference/provinces" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/reference/provinces", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/reference/cantons

Cantons, optionally of one province

Any valid key.

Parameters

  • provinceCode (in the query)string

curl

curl "https://api.yellowecuador.com/api/public/v1/reference/cantons" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/reference/cantons", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/reference/parishes

Parishes of one canton

Any valid key.

Parameters

  • cantonCode (in the query, required)string

curl

curl "https://api.yellowecuador.com/api/public/v1/reference/parishes" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/reference/parishes", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/reference/property-types

Property types allowed per category

Any valid key.

curl

curl "https://api.yellowecuador.com/api/public/v1/reference/property-types" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/reference/property-types", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/reference/features

Feature codes a listing may carry

Any valid key.

curl

curl "https://api.yellowecuador.com/api/public/v1/reference/features" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/reference/features", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

Listings

The brokerage’s listings, in any status. Creating, editing and publishing go through the same rules and the same plan limits as the dashboard.

GET/listings

The brokerage's listings, any status, newest first

Scope needed: listings:read

Parameters

  • limit (in the query)integer
  • cursor (in the query)string (uuid)
  • updatedSince (in the query)string (date-time)
  • status (in the query)"DRAFT" | "ACTIVE" | "PAUSED" | "SOLD" | "RENTED" | "ARCHIVED"

curl

curl "https://api.yellowecuador.com/api/public/v1/listings" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/listings/{id}

One listing

Scope needed: listings:read

Parameters

  • id (in the path, required)string (uuid)

curl

curl "https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 429.

POST/listings

Create a draft listing

Scope needed: listings:write

The listing is created as a draft owned by the key's acting owner, or by `ownerId`. It is published with a separate call, under the same rules and plan limits as in the dashboard. A brokerage may create 600 listings an hour through the API (more with a higher rate agreed in its contract; past it, 429) and hold 500 drafts made through the API (2 000 with bulk import; past it, 409 `too_many_drafts`). Neither uses the owner's own budget in the dashboard.

Parameters

  • Idempotency-Key (header)string

Body (JSON)

  • operation (required)"SALE" | "RENT" | "SHORT_TERM"
  • category (required)"RESIDENTIAL" | "COMMERCIAL" | "LAND" | "NEW_CONSTRUCTION"
  • propertyType (required)"HOUSE" | "APARTMENT" | "SUITE" | "PENTHOUSE" | "TOWNHOUSE" | "FARM" | "OFFICE" | "RETAIL" | "WAREHOUSE" | "BUILDING" | "HOTEL" | "LAND"
  • title (required)string
  • descriptionstring | null
  • pricenumber | null
  • priceNegotiableboolean
  • hoaFeenumber | null
  • provinceCodestring | null
  • cantonCodestring | null
  • parishCodestring | null
  • addressLinestring | null
  • neighborhoodstring | null
  • latnumber | null
  • lngnumber | null
  • showExactLocationboolean
  • bedroomsinteger | null
  • bathroomsinteger | null
  • halfBathroomsinteger | null
  • parkingSpacesinteger | null
  • areaM2number | null
  • lotAreaM2number | null
  • floorsinteger | null
  • floorNumberinteger | null
  • yearBuiltinteger | null
  • furnishedboolean
  • petsAllowedboolean
  • featuresarray of "POOL" | "GYM" | "BBQ" | "GARDEN" | "TERRACE" | "BALCONY" | "ELEVATOR" | "SECURITY_24H" | "GATED_COMMUNITY" | "AIR_CONDITIONING" | "HEATING" | "FIREPLACE" | "LAUNDRY" | "STORAGE" | "FURNISHED" | "PET_FRIENDLY" | "SOLAR_PANELS" | "WATER_TANK" | "GENERATOR" | "PLAYGROUND" | "VIEW_CITY" | "VIEW_MOUNTAIN" | "VIEW_OCEAN" | "VIEW_RIVER" | "PARKING_VISITORS" | "DISABLED_ACCESS"
  • projectNamestring | null
  • deliveryDatestring (date) | null
  • videoUrlstring (uri) | null
  • virtualTourUrlstring (uri) | null
  • pricePeriod"NIGHT" | "WEEK" | "MONTH" | null
  • priceOnRequestboolean
  • seismicCondition"NOT_STATED" | "NO_VISIBLE_DAMAGE" | "INSPECTED" | "REPAIRED"
  • seismicYearinteger | null
  • ownerIdstring (uuid)

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/listings" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"operation":"SALE","category":"RESIDENTIAL","propertyType":"HOUSE","title":"Casa de tres dormitorios en Cumbayá","price":185000,"provinceCode":"17","cantonCode":"1701"}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"operation":"SALE","category":"RESIDENTIAL","propertyType":"HOUSE","title":"Casa de tres dormitorios en Cumbayá","price":185000,"provinceCode":"17","cantonCode":"1701"}),
});
const data = await res.json();

Answers 201 when all is well. Possible errors: 400, 401, 403, 409, 422, 429.

PATCH/listings/{id}

Change a listing's content

Scope needed: listings:write

Parameters

  • id (in the path, required)string (uuid)

Body (JSON)

  • operation"SALE" | "RENT" | "SHORT_TERM"
  • category"RESIDENTIAL" | "COMMERCIAL" | "LAND" | "NEW_CONSTRUCTION"
  • propertyType"HOUSE" | "APARTMENT" | "SUITE" | "PENTHOUSE" | "TOWNHOUSE" | "FARM" | "OFFICE" | "RETAIL" | "WAREHOUSE" | "BUILDING" | "HOTEL" | "LAND"
  • titlestring
  • descriptionstring | null
  • pricenumber | null
  • priceNegotiableboolean
  • hoaFeenumber | null
  • provinceCodestring | null
  • cantonCodestring | null
  • parishCodestring | null
  • addressLinestring | null
  • neighborhoodstring | null
  • latnumber | null
  • lngnumber | null
  • showExactLocationboolean
  • bedroomsinteger | null
  • bathroomsinteger | null
  • halfBathroomsinteger | null
  • parkingSpacesinteger | null
  • areaM2number | null
  • lotAreaM2number | null
  • floorsinteger | null
  • floorNumberinteger | null
  • yearBuiltinteger | null
  • furnishedboolean
  • petsAllowedboolean
  • featuresarray of "POOL" | "GYM" | "BBQ" | "GARDEN" | "TERRACE" | "BALCONY" | "ELEVATOR" | "SECURITY_24H" | "GATED_COMMUNITY" | "AIR_CONDITIONING" | "HEATING" | "FIREPLACE" | "LAUNDRY" | "STORAGE" | "FURNISHED" | "PET_FRIENDLY" | "SOLAR_PANELS" | "WATER_TANK" | "GENERATOR" | "PLAYGROUND" | "VIEW_CITY" | "VIEW_MOUNTAIN" | "VIEW_OCEAN" | "VIEW_RIVER" | "PARKING_VISITORS" | "DISABLED_ACCESS"
  • projectNamestring | null
  • deliveryDatestring (date) | null
  • videoUrlstring (uri) | null
  • virtualTourUrlstring (uri) | null
  • pricePeriod"NIGHT" | "WEEK" | "MONTH" | null
  • priceOnRequestboolean
  • seismicCondition"NOT_STATED" | "NO_VISIBLE_DAMAGE" | "INSPECTED" | "REPAIRED"
  • seismicYearinteger | null

curl

curl -X PATCH "https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"price":179000,"description":"Casa luminosa con jardín, a cinco minutos del parque."}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"price":179000,"description":"Casa luminosa con jardín, a cinco minutos del parque."}),
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 422, 429.

POST/listings/{id}/publish

Publish a draft

Scope needed: listings:write

Parameters

  • id (in the path, required)string (uuid)
  • Idempotency-Key (header)string

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/publish" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/publish", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Idempotency-Key": crypto.randomUUID(),
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 402, 403, 404, 409, 422, 429.

POST/listings/{id}/status

Pause, resume, mark sold or rented, archive

Scope needed: listings:write

Parameters

  • id (in the path, required)string (uuid)
  • Idempotency-Key (header)string

Body (JSON)

  • status (required)"ACTIVE" | "PAUSED" | "SOLD" | "RENTED" | "ARCHIVED" | "DRAFT"

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/status" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"status":"PAUSED"}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/status", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"status":"PAUSED"}),
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 402, 403, 404, 409, 422, 429.

POST/listings/{id}/media

Add a photo from a URL

Scope needed: listings:write

The image is downloaded over https from the public internet (JPEG, PNG or WebP, up to 10 MB), then processed like an upload: metadata is removed and the sizes are generated. `status` is PROCESSING until that is done.

Parameters

  • id (in the path, required)string (uuid)
  • Idempotency-Key (header)string

Body (JSON)

  • url (required)string
  • captionstring | null

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/media" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://images.example.com/casa/fachada.jpg","caption":"Fachada"}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/listings/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/media", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"url":"https://images.example.com/casa/fachada.jpg","caption":"Fachada"}),
});
const data = await res.json();

Answers 201 when all is well. Possible errors: 400, 401, 403, 404, 422, 429, 503.

Leads

The leads in the brokerage’s CRM. A lead sent through the API comes in like any other: it joins the open lead that already has that e-mail or phone, or it is created and assigned by the brokerage’s own rules. The answer says its id and whether it was new — so “created”: false tells the caller that a contact with that e-mail or phone exists. The whole lead comes back only to a key that may also read leads; a stage move answers only the id, the stage and the version to a key that cannot.

GET/leads

The brokerage's leads, newest first

Scope needed: leads:read

Parameters

  • limit (in the query)integer
  • cursor (in the query)string (uuid)
  • updatedSince (in the query)string (date-time)
  • stage (in the query)"NEW" | "CONTACTED" | "QUALIFIED" | "APPOINTMENT" | "SHOWING" | "OFFER" | "NEGOTIATION" | "WON" | "LOST"

curl

curl "https://api.yellowecuador.com/api/public/v1/leads" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/leads", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/leads/{id}

One lead

Scope needed: leads:read

Parameters

  • id (in the path, required)string (uuid)

curl

curl "https://api.yellowecuador.com/api/public/v1/leads/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/leads/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 429.

POST/leads

Send a lead in

Scope needed: leads:write

The contact enters the CRM as any inbound lead does: if the brokerage already has an open lead with that e-mail or phone, this request is added to it (`created: false`); otherwise a lead is created and routed by the brokerage's rules. The answer is `{ id, created }`; the whole lead is in `lead` only when the key also holds `leads:read`. Note that `created: false` tells the caller that the brokerage already has an open lead with that e-mail or phone — the CRM's rule for every inbound source.

Parameters

  • Idempotency-Key (header)string

Body (JSON)

  • name (required)string
  • emailstring (email)
  • phonestring
  • listingIdstring (uuid)
  • messagestring
  • ownerIdstring (uuid)
  • locale"es-EC" | "en"

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/leads" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"name":"María Andrade","email":"maria@example.com","phone":"+593 99 123 4567","message":"Quisiera visitar la casa el sábado."}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/leads", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"name":"María Andrade","email":"maria@example.com","phone":"+593 99 123 4567","message":"Quisiera visitar la casa el sábado."}),
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 409, 422, 429.

POST/leads/{id}/stage

Move a lead to another stage

Scope needed: leads:write

The answer is the whole lead when the key also holds `leads:read`; otherwise `{ id, stage, version }`.

Parameters

  • id (in the path, required)string (uuid)
  • Idempotency-Key (header)string

Body (JSON)

  • stage (required)"NEW" | "CONTACTED" | "QUALIFIED" | "APPOINTMENT" | "SHOWING" | "OFFER" | "NEGOTIATION" | "WON" | "LOST"
  • lostReason"PRICE" | "FINANCING" | "CHOSE_OTHER" | "NOT_RESPONSIVE" | "NOT_QUALIFIED" | "TIMING" | "OTHER" | null
  • lostNotestring | null
  • dealobject
  • versioninteger

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/leads/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/stage" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"stage":"CONTACTED"}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/leads/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/stage", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"stage":"CONTACTED"}),
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 409, 429.

POST/leads/{id}/notes

Add a note to a lead

Scope needed: leads:write

Notes are visible to the team in the dashboard. The API never returns notes.

Parameters

  • id (in the path, required)string (uuid)
  • Idempotency-Key (header)string

Body (JSON)

  • body (required)string

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/leads/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/notes" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"body":"Llamó por la tarde; prefiere visitas los sábados."}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/leads/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f/notes", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"body":"Llamó por la tarde; prefiere visitas los sábados."}),
});
const data = await res.json();

Answers 201 when all is well. Possible errors: 400, 401, 402, 403, 404, 429.

Appointments

The brokerage’s appointments. Read only.

GET/appointments

The brokerage's appointments, newest first

Scope needed: appointments:read

Parameters

  • limit (in the query)integer
  • cursor (in the query)string (uuid)
  • updatedSince (in the query)string (date-time)
  • status (in the query)"REQUESTED" | "CONFIRMED" | "DECLINED" | "CANCELLED"
  • from (in the query)string (date-time)

curl

curl "https://api.yellowecuador.com/api/public/v1/appointments" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/appointments", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

GET/appointments/{id}

One appointment

Scope needed: appointments:read

Parameters

  • id (in the path, required)string (uuid)

curl

curl "https://api.yellowecuador.com/api/public/v1/appointments/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/appointments/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 429.

Webhook endpoints

Manage from your code the same endpoints you see in the dashboard.

GET/webhook-endpoints

Webhook endpoints

Scope needed: webhooks:manage

Only the endpoints this key created are listed (another key's, or one made in the dashboard, is not this key's to manage); URLs are shown masked.

Parameters

  • limit (in the query)integer
  • cursor (in the query)string (uuid)

curl

curl "https://api.yellowecuador.com/api/public/v1/webhook-endpoints" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/webhook-endpoints", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 429.

POST/webhook-endpoints

Create a webhook endpoint

Scope needed: webhooks:manage

The signing secret is in this response and nowhere else: a retry with the same Idempotency-Key answers the same endpoint without it (`secretShown: false`). Receiving an event is reading its resource, so the key needs the read scope of every event family it subscribes to: `leads:read` for `lead.*` and `whatsapp.click`, `appointments:read` for `appointment.*`, `listings:read` for `listing.*` and `review.*`.

Parameters

  • Idempotency-Key (header)string

Body (JSON)

  • url (required)string
  • descriptionstring
  • events (required)array of "lead.created" | "lead.updated" | "lead.stage_changed" | "lead.assigned" | "lead.deleted" | "listing.published" | "listing.updated" | "listing.status_changed" | "listing.deleted" | "appointment.created" | "appointment.status_changed" | "review.published" | "whatsapp.click"
  • kind"GENERIC" | "ZAPIER" | "MAKE" | "N8N"

curl

curl -X POST "https://api.yellowecuador.com/api/public/v1/webhook-endpoints" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Idempotency-Key: 7c1f0d3a-example-key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/leads","events":["lead.created","lead.stage_changed"]}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/webhook-endpoints", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({"url":"https://example.com/hooks/leads","events":["lead.created","lead.stage_changed"]}),
});
const data = await res.json();

Answers 201 when all is well. Possible errors: 400, 401, 402, 403, 409, 422, 429, 503.

GET/webhook-endpoints/{id}

One webhook endpoint

Scope needed: webhooks:manage

Parameters

  • id (in the path, required)string (uuid)

curl

curl "https://api.yellowecuador.com/api/public/v1/webhook-endpoints/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/webhook-endpoints/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 403, 404, 429.

PATCH/webhook-endpoints/{id}

Change, pause or resume a webhook endpoint

Scope needed: webhooks:manage

Parameters

  • id (in the path, required)string (uuid)

Body (JSON)

  • urlstring
  • descriptionstring | null
  • eventsarray of "lead.created" | "lead.updated" | "lead.stage_changed" | "lead.assigned" | "lead.deleted" | "listing.published" | "listing.updated" | "listing.status_changed" | "listing.deleted" | "appointment.created" | "appointment.status_changed" | "review.published" | "whatsapp.click"
  • status"ACTIVE" | "PAUSED"

curl

curl -X PATCH "https://api.yellowecuador.com/api/public/v1/webhook-endpoints/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"PAUSED"}'

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/webhook-endpoints/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"status":"PAUSED"}),
});
const data = await res.json();

Answers 200 when all is well. Possible errors: 400, 401, 402, 403, 404, 422, 429, 503.

DELETE/webhook-endpoints/{id}

Delete a webhook endpoint and its delivery log

Scope needed: webhooks:manage

Parameters

  • id (in the path, required)string (uuid)

curl

curl -X DELETE "https://api.yellowecuador.com/api/public/v1/webhook-endpoints/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f" \
  -H "Authorization: Bearer typ_live_EXAMPLE_REPLACE_WITH_YOUR_KEY"

JavaScript

const res = await fetch("https://api.yellowecuador.com/api/public/v1/webhook-endpoints/3f2c9a1e-5b7d-4c8a-9e21-0a1b2c3d4e5f", {
  method: "DELETE",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
  },
});
if (!res.ok) throw new Error(await res.text());

Answers 204 when all is well. Possible errors: 400, 401, 403, 404, 429.

Webhooks

A webhook is a POST we make to a URL of yours when something changes. Endpoints are created in the dashboard or through the API, and each one chooses which events it receives. Receiving an event is reading its resource: a key subscribes an endpoint only to events it may read (leads:read for lead events and WhatsApp clicks, appointments:read for appointments, listings:read for listings and reviews), and manages only the endpoints it created through the API.

Events

  • lead.createdA new lead came in
  • lead.updatedA lead’s details changed
  • lead.stage_changedA lead moved to another stage
  • lead.assignedA lead went to another agent
  • lead.deletedA lead was deleted
  • listing.publishedA listing was published
  • listing.updatedThe content of a listing that is not a draft changed
  • listing.status_changedA listing changed status
  • listing.deletedA listing was deleted
  • appointment.createdAn appointment was requested or booked
  • appointment.status_changedAn appointment changed status
  • review.publishedA review was published
  • whatsapp.clickSomeone opened WhatsApp from a listing or a page of the brokerage or its agents

The message

“data” is the resource as the API returns it when the message is sent: it is read from the record at that moment, never from a stored copy, so a retry or a resend carries the current state. On changes, “previous” carries the old value of the stage, the status or the agent. A resource deleted in the meantime arrives as its id with “deleted”: true; a *.deleted event, as its id.

{
  "id": "5b0c7a52-6f4e-4d59-9a0e-2f1f0d6c9e11",
  "type": "lead.created",
  "createdAt": "2026-10-03T15:04:05.000Z",
  "apiVersion": "v1",
  "brokerageId": "…",
  "data": {
    "id": "…",
    "name": "María Andrade",
    "email": "maria@example.com",
    "phone": "+593 99 123 4567",
    "stage": "NEW",
    "source": "INQUIRY",
    "owner": {
      "id": "…",
      "name": "Carlos Vera"
    },
    "listingIds": [
      "…"
    ]
  }
}

Signature

Every message carries the X-TYP-Signature header: the send time (t, in seconds) and one or more v1 signatures, each the hexadecimal HMAC-SHA256 of “t.body” with the endpoint’s secret. Check the signature over the exact body you received, compare in constant time, and refuse messages whose t is more than 300 seconds from your clock. After a secret rotation two signatures arrive for 24 hours: the new secret’s and the previous one’s.

X-TYP-Signature: t=1790000000,v1=<64 hex>
import { createHmac, timingSafeEqual } from "node:crypto";

// `rawBody` is the exact bytes received — verify before parsing the JSON.
export function isAuthentic(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = String(header).split(",").map((p) => p.trim().split("="));
  const t = Number(parts.find(([k]) => k === "t")?.[1]);
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return signatures.some((s) => {
    const given = Buffer.from(s, "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

// isAuthentic(body, req.headers["x-typ-signature"], process.env.WEBHOOK_SECRET)

Delivery

  • We only send to https URLs on the public internet, on port 443. We do not follow redirects.
  • Your server has 5 seconds to answer with a 2xx code. Any other answer counts as a failure.
  • A failure is retried until 8 attempts have been made over about 24 hours (after 1 min, 5 min, 30 min, 2 h, 5 h, 8 h and 8 h).
  • If an endpoint keeps failing for more than 24 hours (20 failed attempts, counted at most once a minute; a problem on our side never counts) we switch it off and e-mail the owners. While it is off, no events are kept for it; once it is back on, “Send again: everything that failed” resends the failures of the last 7 days.
  • When 1000 messages are waiting for one endpoint, new ones are recorded as failed instead of queued, and the owners are told once a day.
  • Messages may arrive twice or out of order. Use the event id so you never process one twice.
  • The dashboard keeps the delivery log of the last 30 days and lets you send any of them again.