Skip to main content
A property is an own brand: a brand the organisation owns and tracks. POST /orgs/{orgId}/properties creates the own brand, links its competitors and checks the markets you plan to track, in one call. Use it when you onboard a brand from your own system — as a platform partner adding a client’s property, or as an agency adding a client brand.
The call needs a key with the write scope. A partner key also needs a read_write link to the organisation. Without them the API answers 403 write_scope_required or 403 link_scope_insufficient, before it reads the body. See Authentication.

What one call does

The API works through the request in this order:
  1. It validates the body.
  2. It checks that nothing blocks the new brand: the domain, your external_ref, and the markets against the organisation’s country cap.
  3. It creates the own brand. The plan’s brand cap is checked here.
  4. It links the competitors one at a time, in the order you sent them.
A refusal in steps 1 to 3 rejects the whole request and leaves nothing behind. Step 4 is different: it can stop part-way, and the brand from step 3 stays. Partial success explains how to read that response.

Request body

string
required
The property’s display name. 1 to 200 characters. Leading and trailing whitespace is trimmed.
string
required
The property’s website. Send a host name or a URL, up to 2048 characters.The value is normalised before it is stored or compared: it is lower-cased, and the scheme, a leading www., the path and the port are removed. https://www.Example.com/ and example.com are the same brand. What remains must be a valid host name such as example.com. Anything else is 400 validation_error with details[].code set to invalid_domain.
string or null
A hint for the property’s primary market, for example GB. Free text, 1 to 100 characters after trimming. Send null, or leave the field out, for no value; an empty string is refused. It is stored on the brand and is not checked against the list of supported markets. It does not decide where prompts are scanned — each prompt carries its own countries.
string or null
Your own identifier for the property, for example PROP-01342. 1 to 255 characters after trimming, and unique within the organisation — compared without regard to case. Send null, or leave the field out, for no value; an empty string is refused. It comes back as external_ref on the brand, and as brand_external_ref in webhook events.
object[]
default:"[]"
The competitors to track for this property. At most 100 entries per request. That is a limit on the size of one request. It is not your plan’s competitor cap, which applies as the competitors are linked.
string
required
The competitor’s display name. 1 to 200 characters.
string
required
The competitor’s website, normalised in the same way as the property’s domain. A competitor on the property’s own domain is 400 validation_error with details[].code set to competitor_is_self.
string[]
default:"[]"
The countries you intend to run prompts in, as ISO 3166-1 alpha-2 codes such as GB. Case does not matter; codes are upper-cased and duplicates are dropped. The list cannot be longer than the list of markets Surfais scans. A code Surfais does not scan is 400 validation_error with details[].code set to unsupported_country. See Countries & markets.
Markets are validated and echoed back, but not stored. Countries belong to prompts: the organisation starts tracking a market only when an active prompt lists it. Send markets to learn, before anything is created, whether the plan has room for the countries you are about to use in your prompts.
Only name and domain are required. Unknown fields are ignored.

Send the request

Send an Idempotency-Key header with the call. If the connection drops before you see the response, send the same request again with the same key. When the first attempt has completed, the API replays its answer instead of running the request a second time.
Reuse a key only to retry the same request after a timeout, a dropped connection, a 5xx or a 409 idempotency_key_in_flight. A 4xx answer is stored under the key and replayed. So after a refusal that you have fixed — a freed cap slot, a corrected body — send the request under a new key. See Idempotent requests.
The cURL sample prints the response with its headers. The Python and Node.js samples then sort the answer into one of three outcomes: created, partly created, or already there.

The 201 response

data is the brand in full — the same object GET /orgs/{orgId}/brands/{brandId} returns — plus markets.
string[]
The markets you sent, upper-cased and with duplicates removed. Every one of them fits within the country cap.
string[]
The distinct markets the organisation’s active prompts already held when the request arrived.
integer or null
The organisation’s effective country cap. null when caps are not enforced for the organisation. The number in the example is illustrative; the organisation’s current cap is whatever this field reports.
last_scan_at stays null until the results of the property’s first scan are final. The data model describes the other brand fields.

Refused before anything is created

Each of these answers means that no brand and no competitor was created. Branch on error.code, not on the status: this endpoint uses 409 and 422 for refusals that created nothing and for a partial success that did. Each of these answers is stored under the Idempotency-Key you sent. Once you have fixed the cause, send the request again under a new key: the old key replays the refusal. See Idempotent requests. On brand_exists and external_ref_exists, read the brand named by existing_id with GET /orgs/{orgId}/brands/{brandId} and carry on from there instead of creating it. That is also what happens when you repeat a provision call that had already succeeded without sending the same Idempotency-Key. existing_id is present except in a rare race between two requests. If it is missing, find the brand in GET /orgs/{orgId}/brands by its domain or its external_ref. That list leaves out archived brands. country_cap_exceeded names both sides of the sum, so you can see which market does not fit. attempted is the number of markets the organisation would hold.
The numbers in this example are illustrative. The organisation’s current cap is whatever details.cap reports, and usage on GET /orgs/{orgId} shows its live usage and caps at any time. Plans & limits explains what each cap counts. For the errors every endpoint shares, see Errors.

Partial success

A 422 competitor_cap_exceeded from this endpoint means the brand was created. Never send the provision call again after it. Take details.brand_id and add the remaining competitors one by one with POST /orgs/{orgId}/brands/{brandId}/competitors.
The brand is created first. The competitors are then linked one by one, and each link is its own operation. When the plan’s per-brand competitor cap refuses one of them:
  • linking stops, because every later competitor would hit the same cap;
  • the brand stays, and so does every competitor linked before the refusal;
  • the refused competitor leaves nothing behind;
  • the response is 422 competitor_cap_exceeded, and details tells you exactly what was applied.
details has three fields:
string
The id of the brand that was created.
object
The created property, in the same shape as data in a 201. Its competitors list holds the ones that were linked.
object[]
One entry per competitor you sent, in request order, after duplicate domains were removed. name is as you sent it. domain is as you sent it, normalised — match your own records on domain. status is one of:
  • applied — linked. The entry carries competitor_id and reused.
  • failed — refused. The entry carries error, with a code and a message.
  • not_attempted — an earlier competitor hit the cap, so this one was never tried.
In this example the request named three competitors, and the cap refused the second.
The numbers in this example are illustrative. A message is written for people and can carry more detail than this one, so do not parse it. The organisation’s current competitor cap is usage.competitors.cap on GET /orgs/{orgId}.

The same shape under a 409

A competitor can also be refused for a reason other than the cap — for example conflict, when another request linked the same competitor to the new brand at the same moment. The API records that competitor as failed, carries on with the rest, and answers 409 with the same details. error.code is the code of the first competitor that failed. There are no not_attempted entries in this case, because linking did not stop. So the rule that covers every case is short: if error.details.brand_id is present, the brand exists. The samples above branch on that field rather than on a list of codes.

Add the remaining competitors

For each entry whose status is not applied, link the competitor on its own. After a cap refusal you first need room under the cap: stop tracking another competitor with DELETE /orgs/{orgId}/brands/{brandId}/competitors/{competitorId}, or move the organisation to a plan with a higher cap. The cap is usage.competitors.cap on GET /orgs/{orgId}.
A 201 returns the competitor that was linked: id, name, domain and reused. A 409 conflict means the competitor is already tracked for this brand, and details.competitor_id names it. A 422 competitor_cap_exceeded links nothing.
A 500 internal_error or a 503 store_unavailable can also leave the brand behind, when the failure came after the brand was created. Send the call again. If the answer is 409 brand_exists, the first attempt created the brand: read it, compare its competitors with your list, and add the ones that are missing.

How competitors are reused

A competitor belongs to the organisation and can be tracked by several of its own brands.
  • If the organisation already has a competitor on the same domain, compared after normalisation, the API links that one instead of creating a second. The entry reports reused: true, and the competitor keeps its stored name — the name you sent is not applied.
  • Two entries with the same domain in one request collapse into the first.
  • reused appears in details.competitors of a partial response and in the 201 of POST /orgs/{orgId}/brands/{brandId}/competitors. The 201 of the provision call lists the competitors as they are stored, so a reused competitor shows its existing name there.
  • DELETE /orgs/{orgId}/brands/{brandId}/competitors/{competitorId} removes the link between the two brands only. The competitor stays in the organisation and stays readable by id.
See Adding competitors for how tracked competitors feed share of voice.

Next steps

A new property has no prompts, so nothing is scanned yet.

Sync a brand's prompts

Send the prompt set for the new property, with the countries each prompt runs in.

Run a scan

Request the property’s first scan once it has at least one active prompt.

Partners: set your id on the organisation

A partner key can store the partner’s own identifier for a client organisation on its link to that organisation. It comes back as external_ref on GET /orgs and GET /orgs/{orgId}, and as org_external_ref in webhook events.
cURL
Send null to clear the value. This endpoint is for partner keys only, and it follows the same write rule as every other write: the key needs the write scope and the link must be read_write. The write rule is checked first, so a read-only organisation key gets 403 write_scope_required, and an organisation key that can write gets 403 partner_only.