This is the single source of truth for the public lead-intake endpoint, and the version to hand to external developers. The machine-readable twin isprovident-lead-intake-openapi.jsonnext to this file — keep the two in step when either changes. (Two earlier write-ups,docs/PUBLIC_LEADS_ENDPOINT.mdandpublic-lead-full-example.md, were folded into this document and deleted on 2026-07-31; nothing else describes this endpoint.)
Changed 2026-09-06 — read this if you are already integrated.Server-to-server API for pushing leads into the Provident CRM. Two endpoints are involved:Changed 2026-08-24 — read this if you are already integrated.
- New
PUT /v2/public/leads/{id}(§9) — send details you did not have when you created the lead, most obviously quiz answers the customer finished afterwards. Send only what changed; phones and emails are added, never replaced; the lead’s stage, status, owner and funnel stay Provident’s.- Sections 9–16 of the previous revision are now 10–17.
Changed 2026-08-19 — read this if you are already integrated.
- Every
201fromPOST /v2/public/leads— including a merged one — now carriesassignedTo: the agent the lead landed on, with the phone and email to reach them (§8). Merged responses previously had only three fields.- New
GET /v2/public/leads/{id}/assignment(§10) tells you who a lead ended up with, and crucially distinguishes “routing hasn’t finished” from “nobody took it”. Read it before assuming an ownerless lead is a failure.- New
POST /v2/public/leads/{id}/response-time(§11) records how long the agent took to respond.- Provident can now call you when a lead is created or assigned — see §12, webhooks. If you take the webhooks you do not need to poll §10 at all.
- A second enquiry from the same phone/email inside ~24h is now merged into the existing lead instead of creating a new one, and that response has a different, much shorter body. Branch on
merged(§7, §8).- A lead that names a listing now inherits that listing’s lead type, developer, location, property type, bedrooms, currency and price — into fields you left empty (§4.4).
GET /v2/public/developersmoved frompagetolimit/offset;pageis now rejected with422, not ignored (§14.2).- There is now a standalone locations endpoint (§14.1).
- New optional field
integrationRef— tag each lead with the integration that produced it (§4.6.1). Worth sending from day one; it is what lets Provident answer “which of your automations sent this” without guessing.
Provident can also call your server when a lead is created or assigned, which
replaces polling #4 entirely — see §12.
Authentication is OAuth 2.0 Client Credentials. There is no user login, no
redirect, no consent screen — your server exchanges a client id + secret for a
bearer token and uses that token to post leads.
1. Environments
TheAll requests and responses are/v2prefix is mandatory.https://devapi.prov.ae/public/leads(without/v2) returns 404. Every path in this document already includes it.
application/json; charset=utf-8. Send UTF-8 —
Arabic and other non-ASCII content is fully supported.
Credentials
You will be issued a separateclient_id / client_secret pair per environment:
Treat the secret like a password: store it in your server-side configuration or a
secrets manager, never in browser JavaScript, a mobile app bundle, or a public
repository. This API must only be called from your backend — calling it from a
browser would expose your credentials and is blocked by CORS.
If your credentials leak, contact Provident and they will be rotated
(the old secret stops working immediately).
2. Get an access token
Request body
Success — 200 OK
Failure — 401 Unauthorized
client_id/client_secret, or the client has been deactivated.
A grant_type other than client_credentials also fails here.
Token handling rules
- Cache the token in memory for
expires_inseconds (refresh ~60s early). Do not request a new token for every lead — token calls count against your rate limit and create a token record on each call. - On any
401from the lead endpoint, fetch a fresh token once and retry the request. If it fails again with401, stop and alert — do not loop. - Tokens can be revoked server-side; always be prepared to re-authenticate.
3. Create a lead
The only hard requirement
At least one entry inWithout a phone or an email the request is rejected withleadPhonesorleadEmails. Everything else is optional.
400, because the CRM
cannot create or match a contact.
Minimum valid request
Recommended request (typical web form / ad lead)
Complete request — every supported field
This is the full surface of the endpoint. Send everything you have — nothing here is required except one phone or email, and every field you can fill improves routing, attribution and reporting on our side. Omit what you don’t have (don’t send empty strings ornull placeholders).
listingIdandreferenceNoboth identify a listing — send whichever you have. If you send both,listingIdwins.metaFacebookis only for leads coming from Meta / Instagram / TikTok lead forms. Leave the whole object out for website and other sources.quizis only for landing pages that ask a question set. Unlike every other field here, its questions do not need to be agreed with us in advance — see §4.9.
4. Field reference
Legend for Match:- exact — free text stored as sent.
- lookup — matched case-insensitively (after trimming) against a list of accepted values configured in the CRM. An unmatched value does not fail the request; see Matching behaviour.
4.1 Contact identity — at least one of phone/email required
4.2 Lead classification
funnel — which pipeline the lead lands in
Provident runs several pipelines beside the main sales one. Leave funnel out and the lead
goes to the main sales pipeline, which is what almost every enquiry wants and what every lead
posted to this API did before the field existed. Send a value only when the enquiry belongs to a
different business line:
Matched case-insensitively, ignoring spaces, hyphens and underscores — so
Show Room,
show_room and showroom are the same funnel. The lead lands on that funnel’s own first stage,
which is not always called the same thing (New Registration on Show Room,
New Seller - Landlord on Landlord/Sellers Listings).
funnel is not derived from leadType, deliberately. The two do not line up: a Tenant
enquiry may belong to Leasing or to PvH, and a Primary Buyer to sales, Show Room or Provident
the Agency. Only you know which business line the enquiry came from, so only you can say.
An unrecognised funnel never fails the request. The lead is created on the main sales
pipeline and the value you sent comes back in intakeUnresolved.funnel. Watch for that — a
typo ("Leasng") is indistinguishable from omitting the field in every other respect, so the
only symptom is a funnel team quietly receiving nothing.
4.3 Requirement / interest
4.4 Listing the lead is about (optional)
A resolved listing fills in what your form did not ask. Every field in this table is
taken from the listing only where your payload left it empty — anything you send
always wins, and a quiz answer (§4.9) mapped to the same field beats the listing too.
This matters beyond convenience: lead type, developer and location are all inputs to
Provident’s routing, so a lead that names a listing is routed on what it actually is
instead of falling through to a catch-all.
Sending a value we then fail to match (an unknown developer name, say) is still
reported in
intakeUnresolved (§5) even though the listing goes on to fill that field —
the mismatch stays visible so it can be corrected on your side.
Linking a listing also makes the lead inherit that listing’s agent as its owner where one
is set; that agent wins over assignedBy (§4.7).
4.5 Source & attribution
4.6 Page context, click ids & UTM
4.6.1 Submission provenance
Stored verbatim on the lead — no lookup, nothing here can end up inintakeUnresolved.
Top-levelAboutformNameis your web form. It is unrelated tometaFacebook.formName(§4.8), which is the Meta lead-form name — send both when both apply. If you send neither, the Meta block fills them. A lead with no top-levelformName/adsetNametakesmetaFacebook.formNameandmetaFacebook.adgroupNameinstead (truncated to 255). So a scenario that just forwards Meta verbatim no longer produces leads with an empty ad set — which is the column these are reported on. Anything you send at the top level wins.
integrationRef. Most senders run more than one thing against this API — several
Make scenarios, a website form and a chatbot, one automation per campaign. When a batch of
leads turns out to be junk, or a field stops mapping, the first question is always which
one produced these, and nothing else in the payload answers it: formName is the visitor’s
form, source is marketing attribution, and both are frequently identical across your whole
estate.
- Keep it stable. It identifies an integration, not a submission — the same scenario sends the same value every time. A per-lead unique id makes it useless for grouping.
- Make it specific, e.g.
make:meta-leadgen-eu,wp-contact-form-7,chatbot-v2. - It is never validated: nothing to match, so it can never appear in
intakeUnresolved(§5) and can never cost you a lead. An unknown value is simply a new value. - Don’t put your own name in it. Who you are is recorded automatically and separately,
from your access token — see below. Values are only distinguished within one sender, so
two partners can both use
scenario-1without colliding.
You are identified automatically. Every lead records the OAuth client it was submitted
with, resolved server-side from your token. There is no field for it and no way to send
one — attribution a sender can write is not attribution. This is also why a client id and
secret must never be shared between two integrations that you would want to tell apart:
use integrationRef for that, or ask Provident for a second client.
4.7 Assignment (optional)
When the lead also resolves a listing (§4.4) and that listing has an agent of its own,
the listing’s agent wins — assignedBy is used only when the listing has none.
distributionType — a routing hint
distributionType records how you would like the lead routed. It is stored on the lead and
only changes who receives it if Provident has configured a routing rule that reads it. Sending
one is safe: a value no rule claims changes nothing, and it is a label you can send today and
Provident can act on later, without you changing anything.
Where a rule does claim the value, it wins. Provident’s routing rules are tried in a set
order, and a lead usually matches the first rule whose conditions fit — but a lead that names a
distribution type goes to the rule that claims that type, ahead of that order. So a lead sent as
Location distribution that also carries a campaign is routed by the location rule, not by the
campaign rule that would otherwise have taken it first. Ask which types are claimed before you
rely on this; it is configuration on our side and it changes.
Accepted values, matched case-insensitively after trimming (the leading word on its own also
works, so campaign is the same as Campaign distribution):
An unrecognised value is stored exactly as you sent it rather than rejected. A lead is never
lost over this field — it simply matches no rule. That also means a typo fails silently, so
check the value if you expect routing to depend on it. Capitalisation is not a typo:
default,
Default and Default Distribution are all the same value, on your side and on ours.
Personal distribution is the one value with behaviour attached today. It marks the lead as
having a named owner, and a lead marked that way is never re-assigned automatically — it will
not be taken off its owner by the system for going untouched, and no follow-up clock runs
against it at all.
It only takes effect together with a resolved assignedBy. The two fields are one
instruction: assignedBy names the owner, Personal distribution says the owner keeps the
lead. If assignedBy is missing, or is an address we cannot match to a current Provident agent,
the lead is still created and routed normally — but the Personal distribution marker is
dropped rather than applied, because a lead exempted from follow-up with nobody on it is a lead
nothing chases. When that happens the value you sent comes back in intakeUnresolved under
distributionType, alongside assignedBy, so the mismatch is visible in the response:
distributionType out, a Meta lead form can carry it instead. A
Distribution or Distribution Type entry in metaFacebook.customFields (§4.8) is read as a
fallback and normalised through the same table above. That is there because the value is
usually set by whoever built the ad form, not by whoever posts the lead — so you do not have to
lift it out of the custom fields yourself. A top-level distributionType always wins.
4.8 metaFacebook — Meta / Facebook / Instagram lead ads
Send this object only when the lead came from a Meta (or TikTok) lead form.
The standard fields above still drive routing; this block preserves the ad-platform
metadata alongside the lead.
It is no longer only stored. Five of these fields are copied onto the lead itself,
where they are searchable, filterable and reportable next to every other lead column:
Matching on the id is what makes a rename safe: rename a campaign in Ads Manager and
its leads stay on the one campaign in the CRM, under the name it was first seen with.
campaignName (§4.5) is a different thing and is still a strict lookup — it is the CRM’s
own campaign, which drives routing.
4.9 quiz — dynamic quiz / survey answers
For landing pages that ask a set of questions (“What is your budget?”, “When do you
want to buy?”, dropdowns, and so on). Unlike every other field in this document, the
questions do not have to be agreed with Provident in advance — a new quiz starts
capturing answers the first time it posts one, with no API change on either side.
Answers are searchable and filterable in the CRM, shown on the lead, and — for
questions Provident maps to a CRM field — used to fill that field on the lead.
The one thing you must get right
Send a stablequestionKey for every question, and a stable optionKey for every
dropdown answer. These are the only language-independent identity an answer has.
Labels are display text: the same question ships in English and Arabic with different
wording, the same bracket reads 2-5 million or ٢-٥ مليون, and copy gets edited.
- Keys must be stable across languages — the Arabic and English versions of one
question send the same
questionKey. - Keys must be stable over time — do not regenerate them when you edit the wording.
- Keys are scoped to the quiz, so
budgetin quiz A andbudgetin quiz B are independent. You do not need globally unique keys.
The block
Each entry in
answers:
How free-text answers are read
Numbers are parsed best-effort, so you do not have to normalize them yourself. All of these land as 2000000:2 million, 2,000,000, 2.000.000, 2 000 000, AED 2M,
2m aed, Dirhams 2M, ٢ مليون, ٢٠٠٠٠٠٠. Ranges keep both ends (2-5 million,
2m to 5m). Currency words in English and Arabic are recognised and recorded separately.
Dates accept ISO-8601 (2026-12-01) and unambiguous text (1 December 2026).
03/04/2026 is deliberately rejected — day-first and month-first are both plausible
and guessing would file half your leads under the wrong month. Send ISO-8601 or a value.
Yes/no answers are recognised across languages (yes/no, نعم/لا, да/нет, …).
Anything that will not parse is kept verbatim and flagged for review at Provident —
it is never dropped, and it never fails your request.
Limits
Up to 60 answers per submission register new questions; anything beyond that is still recorded and flagged, but does not extend the catalogue. A quiz is capped at 100 questions and a question at 200 options. These exist to stop a malformed submission growing the catalogue without bound — a real quiz will not come near them.4.10 Extra fields you send
Any field not listed above is accepted rather than rejected, so adding a field on your side will never start failing your requests. It will not populate a structured CRM field either — if you have data that should drive routing or reporting, ask Provident to add it to this specification first, or send it throughquiz (§4.9),
which is designed for exactly that and needs no change on our side.
5. Matching behaviour and intakeUnresolved
Fields marked lookup are matched case-insensitively (after trimming) against
lists maintained inside the CRM.
The intake is deliberately forgiving — a bad lookup value never loses you a lead:
- Matched → the lead is linked to that record.
- Not matched (or ambiguous) → the lead is still created (
201), the field is left empty, and the original text you sent is echoed back inintakeUnresolvedso it can be reviewed and corrected.
intakeUnresolved whenever it is non-empty.
A field that consistently appears there means your value doesn’t match Provident’s
list — fix the value on your side or ask for the correct spelling. A lead whose
source did not resolve loses its attribution.
funnel (§4.2) is the one worth alerting on rather than merely logging. Every other
unmatched lookup leaves a visibly empty field on the lead; an unmatched funnel instead
files a perfectly complete lead onto the main sales pipeline, which looks exactly like a
lead that never asked for a funnel at all. intakeUnresolved.funnel is the only signal
that a whole business line has stopped receiving its leads.
Quiz entries (§4.9) behave slightly differently from the rest: the answer is
stored on the lead regardless, and a Provident admin can map the value once so every
future submission of it resolves automatically. A quiz key appearing here repeatedly
usually means a free-text question that would be better as a dropdown, or a numeric
question where sending value would remove the guesswork.
A merged response (§7) carries no intakeUnresolved field at all — the enquiry was
folded into an existing lead rather than creating one. Guard for its absence wherever
you log this.
6. How a lead becomes a contact
You never send a contact id. The API resolves the contact for you:- Each value in
leadPhonesis normalised and matched against existing contacts. First match wins. - If no phone matches, each value in
leadEmailsis matched the same way. - If nothing matches, a new contact is created from the name/company/phone/ email fields you sent.
+971501234567) so matching is reliable.
This is not the same question as lead merging (§7). Contact resolution asks “who is this?” and always runs; merging asks “is this the same enquiry?” and only applies inside the merge window. Two leads a month apart share one contact and stay two leads.
7. Duplicates, merging and retries — important
The endpoint is not idempotent at the transport level. There is no request id orIdempotency-Key header: a retried request can reach us twice, and metaLeadId does
not protect you — it only stops the Meta metadata block being attached twice.
What does protect you is intake hygiene, which runs before the lead is created and
decides what a second enquiry from the same person actually is. Matching is on the
normalised phone first, then the email — one more reason to send E.164.
The window is a Provident-side setting (24 hours today). Treat it as “about a day”, not as
a constant to hard-code.
Two consequences you have to handle:
idis not always a new lead. Whenmergedistruethe id you get back is one you already have. Don’t record it as a second submission, and don’t overwrite the original submission’s timestamps with this one’s.- The merged body is short —
id,merged,matchedOnand nothing else. NocontactId, nointakeUnresolved, noneedsIntakeReview, nocreatedAt(§8).
Portal exemption. Enquiries whose source is a property portal (Property Finder,
Bayut, Dubizzle) are never merged: those are listing-driven, so the same person
legitimately enquires about several properties held by different agents. A website or
ad-form source is not exempt — if that is you, expect merges.
Retry policy
- Never retry a request that returned any
2xx,400,401/403or422— the lead was either already created (or merged) or will never be accepted. - Only retry on network timeouts,
429and5xx, with exponential backoff (e.g. 1s, 4s, 15s, max 3 attempts). - A timeout is ambiguous — the lead may have been created. Retry anyway: inside the merge window the retry folds into the first attempt instead of duplicating it, which is exactly the case this protects.
- Store the returned lead
idagainst your own submission record. It is the key Provident will use for any question about a specific lead.
8. Responses
Both outcomes below return201. Branch on merged before reading anything else.
201 Created — a new lead
assignedTo — when you can rely on it
assignedTo is filled in the same transaction that creates the lead in exactly two
cases, and is reliable in both:
- you sent a
referenceNo(orlistingId) that resolved to a listing — the lead goes to that listing’s agent; - you sent
assignedBywith an agent’s email.
assignedTo will be null and that means “not decided yet”, not
“nobody”. Do not treat it as a failure and do not tell the customer anything about it.
To find out who it landed on, either poll §10 or — better — take the lead.assigned
webhook in §12 and be told.
The agent object is the same shape everywhere it appears in this API:
active: false is reachable and you must handle it. Routing never assigns to a
deactivated account, but a lead can be assigned to someone who is deactivated afterwards —
1.4% of leads created in the last 90 days are in that state (read 2026-08-24). The agent is
still returned rather than nulled, because who owns the lead is a fact worth recording. Do
not message them and do not name them to a customer: that promises an introduction nobody
will make. Treat it as you would assignedTo: null for messaging purposes, and keep the
details for your own records.
Personal phone numbers are never returned by any endpoint in this API.
The response carries the whole lead record, well beyond the table above. Those extra
fields are not part of this contract, may change at any time, and must not be used in
your logic.
201 Created — merged into an existing lead
When intake hygiene folds the enquiry into a lead created inside the merge window (§7),
the status is still 201 but the body is only these three fields:
contactId, intakeUnresolved, needsIntakeReview and createdAt are absent here.
Code that reads them unconditionally breaks on the first repeat enquiry.
assignedTo is usually populated on a merged response, and more reliably than on a new
one: the lead already exists, so its owner is already known. A merged enquiry is a second
customer message on a lead whose agent may have been notified once already — if you notify
agents, this is who to notify, and you will also receive a lead.assigned webhook with
trigger: "merged" (§12).
Error format
Every error uses the same envelope:422 adds one more key, errors — an array naming each field that failed validation.
9. Updating a lead — details that arrive later
intakeUnresolved
reporting are exactly those of POST /v2/public/leads (§4) — this is the same body, with
three fields removed (below). Everything is optional, including phone and email: an update
that carries nothing but a quiz block is normal and expected.
The four rules
- A field you omit is left alone. This is a PATCH in behaviour whichever verb you
use;
PUTdoes not blank the fields you left out. - A field you send wins over what the lead already had, including a value an agent typed. You are the source; do not send a field you are not authoritative about.
- An explicit
nullclears the column."budgetMax": nullempties it; omittingbudgetMaxdoes not. - A value that matches no CRM record leaves the column as it was and is reported in
intakeUnresolved. A typo’d developer name never erases the developer already on the lead.
Phones and emails are added, never replaced
leadPhones and leadEmails are appended. A number or address the lead already has is
ignored (matching is on the normalised form, so formatting differences do not duplicate);
anything new is added. Nothing is ever deleted, and the primary number only changes when
there was not one before. Re-sending the same phone on every update is therefore free.
Quiz answers replace, per quiz
Answers for aquizKey you send supersede whatever that quiz previously recorded on
the lead, so re-sending the same submission is idempotent and a corrected answer replaces
the wrong one. Quizzes you do not mention are untouched. A quiz answer mapped to a lead
field (budget, area, bedrooms…) is promoted onto the lead under the same precedence as on
create: a field you sent explicitly in the same request beats the answer.
What you cannot change
The lead’s workflow belongs to Provident and is not writable here, whatever you send: stage, status, the agent it is assigned to, the funnel board it sits on, and the aging clock. Three create-only fields are therefore absent from this body:
Sending them anyway is not an error — they are ignored, and kept verbatim in the lead’s
intake record.
Routing
New details do send a lead nobody holds back through the assignment engine — the answers you just sent (area, developer, lead type, budget, languages) are exactly what it matches on, so a lead that could not be placed when all we knew was a phone number gets a second chance. The response says so inrerouted, and the outcome is asynchronous: poll
§10 or wait for the lead.assigned webhook.
A lead an agent already holds is never taken off them by an update.
The agent is told
Every update that changes something posts a note on the lead’s own timeline naming your integration and what changed, so the agent working it sees that new details arrived rather than discovering them by chance.Request
Response — 200 OK
Errors
There is no
422 here: like POST /public/leads, this endpoint does not reject a body over
a field it does not recognise or cannot match.
Notes worth reading once
- Use the
idfrom the create response — including a merged one, where the id is the existing lead you were folded into (§7). Updating it updates that lead, which is usually what you want and always what you asked for. - This is not a way to avoid the merge window. If the customer enquires again, POST it; a second POST inside the window is merged and keeps both enquiries. An update is for more detail about the same enquiry, not for a new one.
- Order does not matter, but time does. Nothing stops a lead being updated after an agent has worked it for a week. Rule 2 still applies — you will overwrite what they changed, so only send fields you own.
- Every update is recorded in Provident’s audit log with your client, the changed field list, and the verbatim body.
10. Who a lead was assigned to
{id} is the lead id from the create response — a new one or a merged one, both work.
Why this exists. For most leads the agent is chosen by a routing engine that runs
after POST /v2/public/leads has answered you. So a lead with no owner on the create
response is normal, and the question you actually need answered is not “is there an agent”
but “is there going to be one”. That is what status tells you.
Response — 200 OK
The four states — branch on status, not on agent
agent is non-null only when status is assigned. retryAfterSeconds is non-null only
when status is pending.
unassignedandpoolare common — roughly 30% of leads. They are not errors and not something to retry your way out of. Treating them as “still routing” leaves a customer waiting forever for an introduction that is not coming.
Polling
If you are not using the webhooks in §12, poll like this:- First check ~30 seconds after creating the lead. Routing is normally much faster, but there is no benefit to asking sooner.
- While
statusispending, waitretryAfterSecondsand ask again. - Stop at ~2 minutes. Past that,
pendingwill have resolved to something final on its own; a lead stillpendingthen is escalating and may take much longer. assigned,unassignedandpoolare all final answers — stop polling.
lead.assigned webhook — polling will not tell you.
What it does not return
Only the agent’s work contacts and the lead’s status. Nothing about the customer, the enquiry, or the lead’s content. If you need those, you already have them — you sent them.Errors
11. Recording the agent’s response time
Response — 200 OK
agentRespondedAt is set by Provident at the moment of the call, not by you. It is what
distinguishes a genuine 47-second response from a figure backfilled three days later.
Last write wins. Calling this twice for the same lead overwrites; it is safe to retry.
Timing is not sensitive — minutes or hours after the fact is fine.
A lead with no assignee is accepted, not rejected. A tracking link can be clicked by an
agent who claimed the lead out of the pool, or by a previous owner, and the lead’s assignee
at the moment you write is not necessarily who responded. Rejecting those writes would lose
exactly the slowest responses — the ones the metric exists to surface — so the endpoint
records the number regardless of status.
Errors
12. Webhooks — being told instead of asking
Provident can POST to your endpoint when something happens to a lead. This replaces polling §10 entirely and is the recommended integration. To switch it on, send Provident:- a URL for each event you want (they can be the same URL);
- whether you want a shared secret, and what to call the header if not the default.
Authentication
Every call carries the shared secret in a header:401. The secret is the only
thing proving the call came from Provident — nothing in the body is authentication.
The two events
Nothing is sent when a lead ends up with no owner. “Nobody took it” is a state, not
an event — ask §10 for it. If you are waiting for a lead.assigned that never comes,
that is the answer, and it is why you should still have a timeout.
lead.created will include the leads you sent us, unless you ask Provident to
exclude your API client — which you almost certainly want, or you will greet the same
customer twice. Say so when you request the webhook.
lead.created
property is a display name, not an id — it is meant to go straight into
a message a person reads. community is null for some leads (the location tree is
uneven), which is why location and locationPath travel alongside it; fall back to those
rather than leaving a hole in a sentence.
agent may be null: this event fires when the lead is created, which is often before it
has an owner. Wait for lead.assigned.
lead.url and lead.pageUrl are not interchangeable. url opens the lead inside the
Provident CRM and is for staff. pageUrl is the public page the enquiry came from — the
listing or project page the customer was actually looking at — and is the link to put in a
message to an agent. It is null when there was no public page (Meta lead-ads, leads
created inside the CRM by staff); render that as “N/A” rather than falling back to url,
which an agent may not be able to open.
lead.assigned
Same lead, customer, property and agent blocks, plus:
agent is always populated on this event.
merged is the one worth special-casing: it means a second customer message arrived on
a lead whose agent you may already have notified once. The customer is not new; the
enquiry is.
Your endpoint’s contract
- Answer
2xxas soon as you have stored the event. Do the work afterwards. Provident waits 10 seconds and no longer. 4xxis permanent. Anything other than408or429is taken as “understood and refused” and is never retried. Do not use400for a temporary problem.5xx,408,429and timeouts are retried at 30s, 1m, 5m, 10m, 15m, 30m, then given up on. A partner outage of about half an hour loses nothing.- Be idempotent on
deliveryId. A retry after your2xxwas lost in transit is indistinguishable from a first attempt, so at-least-once is the guarantee — not exactly-once. - Order is not guaranteed. A
lead.assignedthat was retried can arrive after a laterreassignedfor the same lead.occurredAtis stamped when the body was built and is what to compare.
13. Rate limits
Every authenticated request is counted per client, per UTC day. The response carries the current state:
Exceeding it returns
429 Too Many Requests with a Retry-After header (seconds).
The default quota is modest and is raised per client on request — tell Provident
your expected daily lead volume before go-live so the limit is set correctly.
Token requests count too, which is another reason to cache the token.
IP allowlisting (optional)
Provident can restrict a client to a set of source IPs / CIDR ranges. If that is enabled for you, requests from anywhere else get403 Forbidden with the offending
IP in the message. Send your production egress IPs when you request credentials.
14. Reference data endpoints (optional)
Alongside lead submission you can read Provident’s locations, developers and projects. Use them to populate dropdowns on your side, to sendareaOfInterest /
propertyTypeInterest values that match ours, or to show project content on your
landing pages.
All of them:
- use the same bearer token as the lead endpoint — no extra credentials,
- are
GETand read-only, - count against the same rate limit, so cache the results (these lists change rarely — a daily refresh is plenty; do not call them per form submission).
The two paging styles are not interchangeable. Locations and developers takelimit/offset; projects still takepage/limit. An unrecognised query parameter is rejected, not ignored — sending?page=to the developers endpoint returns422 VALIDATION_ERROR(property page should not exist), not page 1.
14.1 Locations
Two ways in, for two different jobs. A type-ahead picker — search the whole community / area tree:GET /v2/public/locations/{id} returns one node in the same shape, for re-displaying a
location the visitor picked earlier.
path is the full ancestry ("Ajman, Al Zorah, Marina") and is there because names are
not unique — the tree has thousands of nodes and hundreds of repeated leaf names. Show
path in your picker so the user can tell two “Marina”s apart.
Send theThe bulk lookup lists — locations, developers, property types and statuses in one call, which is what you want for pre-populating static dropdowns:name, not the id. The lead payload has no location-id field:areaOfInterest(§4.3) is matched by name. And a name that matches more than one node is treated as unmatched — the lead is still created, the field is left empty and the text comes back inintakeUnresolved(§5). If your picker offers an area whosenameis not unique, expect that field not to link.
propertyTypeInterest against the filters call, and areaOfInterest
against whichever of the two you populated your form from.
14.2 Developers
Changed 2026-08. This endpoint used to takepageand returnmeta: { page, limit, total, totalPages }. It now takeslimit/offsetand returnsmeta: { limit, offset, total }.pageis no longer accepted at all — sending it returns422 VALIDATION_ERROR, it is not ignored. Update any integration written against the old shape.
GET /v2/public/developers/search is the same endpoint with relevance ranking applied;
it takes exactly the same parameters and falls back to the plain list when no term is
given.
GET /v2/public/developers/{id} returns a
single developer, or 404 if it does not exist or is disabled.
14.3 Projects
The UUIDs for
developerIds, locationIds, statusIds and propertyTypes all come
from GET /v2/public/projects/filters.
GET /v2/public/projects/{id} returns one project (?simplified=false for the full
record), 404 if it is not published. GET /v2/public/projects/map takes required
latMin, latMax, lngMin, lngMax bounds plus the same filters and returns map
pins.
14.4 Attaching this data to a lead
Developers map directly. Send the developer’sname (or slug) as
developerName, or its UUID as developerId — see §4.5. Locations and property
types map by name only (there is no id field for either on a lead): send the name
from the filters call — or, for locations, from GET /v2/public/locations/search — as
areaOfInterest and propertyTypeInterest. A location name shared by more than one node
in the tree cannot be matched; see §14.1.
Projects have no top-level field. Sending projectId at the top level is accepted
but ignored (§4.10). Two ways to tie a lead to a project:
- Via a quiz question (§4.9). Ask the project as a quiz question and have Provident map that question to the project field — the answer then lands on the lead as a real project link, with no change to this specification. This is the supported route.
- Otherwise, put the project name in
initialInquiry,campaignNameorutmCampaignand it will at least be searchable as text.
Approved listings are also readable —GET /v2/public/listingsandGET /v2/public/listings/filters— which is where thereferenceNoandlistingIdvalues for a lead come from. Ask Provident if you need that flow.
15. Reference implementations
Node.js (18+, built-in fetch)
Python
PHP (cURL)
16. Go-live checklist
- Integration built and tested against
https://devapi.prov.ae/v2. - Credentials stored server-side only (env vars / secrets manager), never in client-side code or version control.
- Access token cached in memory and reused until ~60s before expiry.
-
401triggers exactly one token refresh + retry; no retry loops. - Retries only on timeout /
429/5xx, with exponential backoff. - Returned lead
idstored against your own submission record. -
mergedchecked on every201before readingcontactId,intakeUnresolved,needsIntakeRevieworcreatedAt— a merged response has none of them (§7, §8). - A merged
idrecognised as an existing lead, not recorded as a new one. -
assignedTo: nullon a201understood as “not decided yet”, not “nobody” — the routing engine runs after the response is sent (§8). - If you notify agents: either the
lead.assignedwebhook is wired up (§12) or you pollGET /leads/{id}/assignment(§10) — and in either case you branch on all fourstatusvalues, withunassignedandpoolhandled as final answers rather than retried. - If you take webhooks: your endpoint verifies
X-Provident-Webhook-Secret, answers2xxwithin 10 seconds before doing any work, and is idempotent ondeliveryId. - If you take
lead.created: you asked Provident to exclude your own API client, so the leads you submit are not sent back to you. -
intakeUnresolvedlogged and reviewed — no field appearing there routinely. - Phone numbers sent in E.164 format (
+9715…). -
source,subSource,eventType,marketingTypevalues confirmed against Provident’s accepted lists. - Reference-data calls (§14) use the right paging for each endpoint —
limit/offsetfor locations and developers,page/limitfor projects. An unknown query parameter is rejected with422, not ignored. - If you send
quiz(§4.9): every question has a stablequestionKeyand every dropdown answer a stableoptionKey, identical across languages and unchanged when the wording is edited. Confirm this before launch — retrofitting keys after leads have arrived means merging the duplicates by hand. - If you send
quiz: numeric and date answers send a machinevaluewhere you have one, rather than relying on us parsing the label. - Expected daily volume communicated so the rate limit is sized correctly.
- Production egress IPs shared (if IP allowlisting will be enabled).
- Production credentials issued and base URL switched to
https://prodapi.prov.ae/v2.
17. Support
Send Provident the following when reporting a problem:- Environment (dev / prod) and full request URL
- Timestamp (UTC) of the request
- The
code,messageandpathfrom the error response - The lead
idif one was returned - The request body with personal data redacted
provident-lead-intake-openapi.json (OpenAPI 3.0.3) — import it
into Postman, Insomnia, or a client generator.