Endpointy
Pět GET endpointů, všechny read-only, všechny vracejí JSON. Každá ukázka níž je skutečná odpověď našeho sandbox feedu — curl u ní jde spustit tak, jak je.
Konvence
Platí pro všechny endpointy. Většina vznikla z otázek implementátorů a jedna z nich (význam 404) je důvod, proč se kontrakt dá nasazovat po částech.
Transport a kódování
Jen HTTPS, odpovědi v JSON a UTF-8, Content-Type: application/json. Holé HTTP a neveřejné adresy odmítneme ještě před odesláním požadavku.
Autentizace
Jeden bearer token na konzumenta: Authorization: Bearer <token>. Na své straně ho držte jako hash, nechte ho rotovatelný a řekněte nám, jak dlouhé je okno rotace. Když váš stack neumí číst hlavičky, vezmeme token i v URL — jen nám to řekněte.
Dva tvary cest
Výchozí je cesta {base}/offers/123456. Když váš hosting nepředává PATH_INFO za PHP souborem (na sdíleném hostingu běžné), použijte variantu s parametrem: {base}?upwolf_route=offers/123456. Lomítko uvnitř route zůstává nezakódované, identifikátor se kóduje zvlášť. Vyberte si jednu variantu, řekněte nám ji a držte ji na všech endpointech.
Na poli `version` stojí celá detekce změn
Neprůhledný řetězec, který se změní pokaždé, když se změní obsah nabídky — hash obsahu i časové razítko poslední revize fungují stejně dobře. Nikdy ho nerozebíráme, jen ho porovnáváme s verzí, ze které vznikla kreativa. Když se nemění nikdy, nepřegenerujeme kreativu u upravených nabídek; když se mění při každém dotazu, přegenerováváme pořád všechno.
Chyby a význam 404
Chyba je HTTP status a k tomu {"error": {"code": "…", "message": "…"}}. **404 je platná odpověď**, ne selhání: u nabídky znamená „není ve feedu", u celého endpointu „zatím neumíme". Právě díky tomu jde kontrakt nasazovat po částech — my si zjistíme, co existuje, a zbytek degradujeme. Cokoliv 5xx opakujeme třikrát s exponenciálním backoffem.
Časové limity, se kterými pracujeme
Čtení na pozadí má timeout 20 sekund. Jedna cesta je výrazně přísnější: když kampaň přijde pushem, máme na celou odpověď půl sekundy, takže detail nabídky tahá s rozpočtem 300 ms a jediným pokusem. Feed, který odpoví do 300 ms, dostane cílení hned napoprvé; pomalejší funguje taky, jen u toho jednoho volání spadne na výchozí hodnoty.
Stránkování a `updated_since`
Stránkované endpointy vracejí next_page (jakýkoliv skalár, vracíme ho zpátky beze změny; null = konec). Dvě věci potřebujeme mít popsané: jestli je hranice updated_since inkluzivní nebo exkluzivní a jestli je řazení stabilní. Watermark si posouváme na čas začátku průchodu, takže při exkluzivní hranici a nestabilním řazení položky mezi stránkami tiše propadávají.
Identita a idempotence
external_id identifikuje nabídku a nikdy se nesmí použít pro jinou. (source_type, external_key) identifikuje kandidáta na kampaň. Opakování je vždycky bezpečné: stejná dvojice nikdy nezaloží druhou kampaň, jen aktualizuje tu existující. Proto musí být zrušení výslovné status: "cancelled", ne zmizení z výpisu — zmizení se nedá odlišit od vypadnutí z okna updated_since.
Zavřené nabídky zůstávají čitelné
Když se nabídka zavře, přepněte status na closed, ale detail nechte dál odpovídat. Kampaň, která už běžela, potřebuje data nabídky i poté, co nabídka zmizela — kvůli výsledným metrikám, archivu kreativy a reportu, který vidí klient.
Cachování (volitelné, ale oceníme)
Posílej na výpisech ETag nebo Last-Modified a odpovídej 304, když se nic nezměnilo. Snapshot pulujeme v řádu minut; u velkého katalogu je tohle nejlevnější věc, kterou můžete pro vlastní servery udělat.
Reference
Autoritativní množina pro srovnání: co ve snapshotu není, není aktivní. Procházíme ho v řádu minut a kampaním u nabídek, které z něj vypadly, zastavujeme běh. Bez stránkování a bez parametrů — jestli je váš katalog tak velký, že je to problém, ozvěte se a domluvíme stránkování. Když sem přidáte `name` a `company.name`, ušetříte nám jedno volání detailu na nabídku a celý výpis vznikne z jednoho requestu.
curl -s "https://upwolf.com/dev/sandbox/feed/offers" \
-H "Authorization: Bearer sandbox-demo-token"
{
"items": [
{
"external_id": "DEMO-1001",
"name": "Senior Backend Engineer",
"company": {
"name": "Nimbus Data s.r.o."
}
},
{
"external_id": "DEMO-1002",
"name": "Skladník / skladnice",
"company": {
"name": "Retail Logistics a.s."
}
}
]
}
Pole po poli: OffersSnapshot · JSON Schema
Text reklamy vzniká z `name`, `description` a `company`; cílení z `area_id` a `locations`. Na neznámou nabídku odpověz `404`. U zavřených nabídek odpovídej dál.
curl -s "https://upwolf.com/dev/sandbox/feed/offers/DEMO-1001" \
-H "Authorization: Bearer sandbox-demo-token"
{
"external_id": "DEMO-1001",
"version": "v2026-09-01T10:00:00Z",
"name": "Senior Backend Engineer",
"description": "Hledáme backend vývojáře do týmu, který staví platební bránu pro evropské e-shopy.\n\nCo tě čeká: návrh a vývoj služeb v PHP 8 a Go, práce s PostgreSQL a Kafkou, code review a mentoring juniorů.\n\nCo čekáme: pár let na podobné pozici, zkušenost s návrhem API a chuť starat se o to, co jsi napsal, i v provozu.\n\nNabízíme: čtyři dny v kanceláři jednou za měsíc, zbytek remote, 5 týdnů dovolené a rozpočet na vzdělávání.",
"language_code": "cs",
"status": "published",
"company": {
"name": "Nimbus Data s.r.o.",
"introduction": "Stavíme platební infrastrukturu pro e-commerce ve střední Evropě. 40 lidí, vlastní produkt, žádné agenturní projekty.",
"logo_url": "https://upwolf.com/assets/img/sandbox-logo.png",
"logo_approved": true
},
"salaries": [
{
"minimum": 90000,
"maximum": 130000,
"currency": "CZK",
"measure": "month"
}
],
"area_id": "142",
"area_root_id": "180",
"locations": [
{
"city": "Praha"
},
{
"city": "Brno"
}
],
"updated_at": "2026-09-01T10:00:00+02:00"
}
Pole po poli: Offer · JSON Schema
Stejná cesta jako snapshot, ale s parametrem `updated_since` vrací identitu a `version` místo živé množiny. Každou verzi porovnáme s tou, ze které vznikla kreativa, a kreativu označíme za neaktuální. Bez tohohle endpointu běží upravená nabídka dál s reklamou napsanou ze starého textu, dokud si toho někdo nevšimne.
| Parametr | Typ | Povinný | Poznámka |
|---|---|---|---|
| updated_since | string (ISO 8601) | ano | Dolní hranice; popiš, jestli je inkluzivní. |
| page | scalar | ne | Hodnota `next_page` z předchozí odpovědi. |
curl -s "https://upwolf.com/dev/sandbox/feed/offers?updated_since=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer sandbox-demo-token"
{
"items": [
{
"external_id": "DEMO-1001",
"version": "v2026-09-01T10:00:00Z",
"status": "published",
"updated_at": "2026-09-01T10:00:00+02:00"
},
{
"external_id": "DEMO-1002",
"version": "8f14e45fceea167a",
"status": "published",
"updated_at": "2026-09-03T08:30:00+02:00"
},
{
"external_id": "DEMO-1003",
"version": "v2026-08-20T16:00:00Z",
"status": "closed",
"updated_at": "2026-08-20T16:00:00+02:00"
}
],
"next_page": null
}
Pole po poli: OfferChanges · JSON Schema
Tohle je ta věc, díky které kampaně vznikají samy. Kandidát je vaše byznysové rozhodnutí — placená viditelnost, aktivovaný PPC produkt, cokoliv prodáváte — a nese okno i případné přebití rozpočtu. Nefiltruj podle toho, jestli je nabídka publikovaná; to si vyhodnotíme sami z detailu. Zrušení je `status: "cancelled"` u téže dvojice, nikdy ne zmizení. Výpis zároveň slouží jako srovnávací kolo: když se někdy ztratí push volání, nejbližší průchod to dorovná sám.
| Parametr | Typ | Povinný | Poznámka |
|---|---|---|---|
| updated_since | string (ISO 8601) | ne | Při prvním průchodu se nepošle — tehdy chceme všechno. |
| page | scalar | ne | Hodnota `next_page` z předchozí odpovědi. |
curl -s "https://upwolf.com/dev/sandbox/feed/campaign-candidates?updated_since=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer sandbox-demo-token"
{
"items": [
{
"source_type": "visibility",
"external_key": "98765",
"offer_external_id": "DEMO-1001",
"product": "Extra",
"window": {
"since": "2026-09-01T00:00:00+02:00",
"until": "2026-09-30T23:59:59+02:00"
},
"status": "active",
"budget_override": null,
"delay_override_days": null,
"first_campaign_boost": false,
"updated_at": "2026-09-01T10:00:05+02:00"
},
{
"source_type": "ppc",
"external_key": "98766",
"offer_external_id": "DEMO-1002",
"product": "Ppc",
"window": {
"since": "2026-09-03T00:00:00+02:00",
"until": null
},
"status": "cancelled",
"budget_override": 250000,
"delay_override_days": 0,
"first_campaign_boost": true,
"updated_at": "2026-09-05T11:20:00+02:00"
}
],
"next_page": null
}
Pole po poli: CampaignCandidates · JSON Schema
Dvě čísla rozhodují o tom, kdy přestaneme utrácet. `count` jsou kvalitní reakce — jak si kvalitu definujete, je čistě vaše věc — a podle nich se kampaň ukončí, jakmile překročí práh. `total_count` jsou všechny přihlášky; nikdy podle nich nerozhodujeme, jen díky nim nelže graf, protože „5 kvalitních" z šesti a z šedesáti je něco úplně jiného. Číslo smí i klesnout (stažené reakce) a je to v pořádku: naše rozhodnutí se nevracejí, takže pokles nerozjede zastavenou kampaň.
| Parametr | Typ | Povinný | Poznámka |
|---|---|---|---|
| from | string (ISO 8601) | ano | Počítej reakce od tohoto okamžiku. |
curl -s "https://upwolf.com/dev/sandbox/feed/offers/DEMO-1001/applications-stats?from=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer sandbox-demo-token"
{
"external_id": "DEMO-1001",
"from": "2026-09-01T00:00:00+02:00",
"count": 4,
"total_count": 11
}
Pole po poli: ApplicationsStats · JSON Schema
Chybové odpovědi
Každá odpověď mimo 2xx by měla nést tohle tělo. Zprávu ukazujeme našim provozním lidem, takže ji piště pro člověka, který váš systém nezná.
{
"error": {
"code": "not_found",
"message": "Nabídka 123456 ve feedu není."
}
}