DEVELOPING

Paying for position

GET /property-v1/properties/{paId}/publication/{portal}/positioning
PUT /property-v1/properties/{paId}/publication/{portal}/positioning

Some portals let you pay so a listing shows higher up in the results. The GET gives you everything you need to decide how much; the PUT does it.

It is a separate step from publishing. A listing publishes just the same without this; what changes is where it appears.

Each portal charges for it differently

The mechanism field tells you what to expect, so one piece of code works for all of them:

PortalWhat you pay formechanismspend is
Argenproppoints from a monthly subscription, which the agency distributes across its listingspointsa number
Zonapropthe listing's plantierthe name of a tier
MercadoLibrethe listing typetierthe name of a tier
Cabaprop— does not charge for position——

Today only Argenprop is enabled. The others get a 501, and the message tells the two cases apart: "the portal has no paid positioning" is not the same as "we have not enabled it here yet".


The GET: the context to decide with

GET …/positioning?simulate=5,100&visits=1-10
ParameterWhat it doesDefault
simulatelist of values to simulate: "what position would I get if I spent this?"empty
visitsrange of positions to fetch how many visits each one getsempty

simulate costs one call against your client's quota PER VALUE, and visits costs two. That is why they are not on by default: you ask for them when you need them. Up to 10 values in simulate and 25 positions in visits.

What it answers

{ "portal": "argenpropapi",
  "mechanism": "points",
  "paId": "01M5375T7YS6PC223DWC0YQPGS",
  "current": { "position": 794, "byBedrooms": 310, "indexed": true },
  "bounds":  { "min": 0, "max": 94518 },
  "balance": { "available": 10, "total": 10, "hasSubscription": true,
               "asOf": "2026-10-16T21:10:00Z" },
  "segment": { "propertyType": 1, "operation": 1, "locality": 2102,
               "neighborhood": 20, "bedrooms": 2 },
  "simulation": [ { "spend": 5, "position": 332 }, { "spend": 100, "position": 185 } ],
  "visitsByPosition": [ { "position": 1, "visits": 7991 }, { "position": 2, "visits": 3673 } ],
  "thresholds": [ { "position": 1, "points": 90018 }, { "position": 2, "points": 90017 } ],
  "calls": 9 }
FieldWhat it is
currentWhere the listing stands today. indexed: false means the portal has not indexed it yet
boundsThe floor and the ceiling of what you can spend on this listing
balanceWhat your client has left, with the date we read it
segmentWhich set of listings it is being compared against
simulationOne entry per value you asked for
visitsByPositionHow many visits each position gets
thresholdsHow many points whoever holds each position has
callsHow many calls against your client's quota this query cost

🔴 The figure that makes everything else actionable

visitsByPosition crossed with simulation tells you how many visits each point buys:

position 1 → 7991 visits           with   5 points → position 332
position 2 → 3673 visits           with 100 points → position 185

Position 2 gets less than half of what position 1 gets. And the curve is not linear: the first 5 points moved 462 positions, the next 5 moved 22.


🔴 What we could not find out, we tell you

A block we could not read does not come back as null: it does not come back at all, and its reason is in unavailable.

{ "current": { "position": 794, "indexed": true },
  "balance": { "available": 10, "asOf": "…" },
  "unavailable": [
    { "block": "simulation", "reason": "no_indexado" },
    { "block": "visitsByPosition", "reason": "no_indexado" } ] }

The difference matters: a field that exists and says null would mean "the portal says there is nothing". An absent field with its reason means "we could not find out", which is what happened.

reasonWhat happenedWhat to do
no_indexadothe listing was published very recently and the portal has not indexed it yetwait and ask again. It is not an error
segmento_sin_datos_suficientesthat property's market is too small for the portal to have statisticsnothing: there are no honest numbers to give
segmento_incompletothe property's location is missingcomplete the zone and publish again
a portal codethe portal rejected that readread it: it is the same kind of code as on publishing

current and the PUT keep working even when everything else is missing. The statistics and the simulation come from a part of the portal that may be unavailable; publishing, checking your position and assigning do not depend on it.

If the segment had to be widened, we tell you

When your property's segment has few listings, the portal compares against a broader one. When that happens, we declare it:

{ "segment": { "propertyType": 1, "operation": 1, "locality": 2102,
               "neighborhood": 20, "bedrooms": null },
  "segmentWidened": ["bedrooms"] }

Label the figure with the segment that came back, not the one you asked for. A curve for "Palermo, any number of bedrooms" is not the same as one for "Palermo, 2 bedrooms".


The PUT: assigning

{ "spend": 50 }
{ "portal": "argenpropapi", "mechanism": "points",
  "paId": "01M5375T7YS6PC223DWC0YQPGS",
  "spend": 50, "position": 185, "calls": 3 }

position is the REAL position we read, not the one the simulation promised: we query it after assigning.

But the portal takes a while to recompute it. Measured: assigning 5 points, the simulation said position 463 and the immediate read still returned the previous one. It is not an error and it does not mean it was not applied — the points were already deducted from the balance. If you want to show the effect, ask for the GET again a while later; if you want to show it right away, use the simulation's number and say it is an estimate.

spend: 0 is valid — it means "stop paying". That is why there is no DELETE here: taking the publication down is a different thing.

If the portal has not reindexed yet, position does not come back and we tell you:

{ "spend": 50, "positionUnavailable": "el portal todavía no reindexó el aviso" }

We do not validate your spend against the max we read. That number comes from a read that may be stale, and rejecting you over it would be our copy deciding instead of the portal. The portal is the authority on what it accepts: if it rejects it, we hand you its reason.

Errors

CodeWhat happened
400A malformed parameter. It is validated before calling the portal, so it costs you no quota
409 portal_not_connectedThat account is not connected to the portal
409 no_publicadoThe property is not published there: there is no position to check
404That property is not in your inventory
501That portal has no paid positioning enabled here
502 portal_rechazoThe portal rejected the assignment. reason carries its code