{"openapi":"3.1.0","info":{"title":"UpWolf portal feed","version":"1.0","summary":"The read-only API a portal exposes so UpWolf can turn its listings into ad campaigns.","description":"You implement this; UpWolf consumes it. Every endpoint may answer 404 while you are still building it — we detect what exists and degrade the rest, so the contract can ship in stages.","contact":{"name":"UpWolf","url":"https://upwolf.com/dev"}},"servers":[{"url":"{feed_base}","description":"Base URL of your feed. Path style; for the query style see the docs.","variables":{"feed_base":{"default":"https://example.com/feed/upwolf"}}}],"security":[{"bearerAuth":[]}],"paths":{"/offers":{"get":{"operationId":"offers_snapshot","summary":"Everything that is live right now, in one response.","description":"The authoritative reconciliation set: what is not in the snapshot is not active. We walk it every few minutes and pause campaigns for offers that dropped out. No pagination and no parameters — if your catalogue is large enough that this becomes a problem, tell us and we will agree on paging. Including `name` and `company.name` here saves us one detail call per offer, so the whole listing renders from a single request.\n\n---\n\nSame path as the snapshot, but with `updated_since` it returns identity and `version` instead of the live set. We compare each version against the one a creative was built from and flag the creative as stale. Without this endpoint an edited offer keeps running with the ad written from its old text until someone notices.\n\n**When this endpoint is missing:** Creative is never refreshed after an offer is edited.","x-upwolf-level":"required","parameters":[{"name":"updated_since","in":"query","required":true,"schema":{"type":"string"},"description":"Lower bound; document whether it is inclusive."},{"name":"page","in":"query","required":false,"schema":{"type":"string"},"description":"Value of `next_page` from the previous response."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OffersSnapshot"},"example":{"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."}}]}}}},"404":{"description":"Offer not in the feed, or the endpoint is not implemented yet. Both are valid answers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/offers/{external_id}":{"get":{"operationId":"offer_detail","summary":"Everything needed to write the ad and aim it.","description":"The text of the ad is generated from `name`, `description` and `company`; the targeting comes from `area_id` and `locations`. Answer `404` for an offer you do not know. Keep answering for closed offers.","x-upwolf-level":"required","parameters":[{"name":"external_id","in":"path","required":true,"schema":{"type":"string"},"description":"Stable offer id."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Offer"},"example":{"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"}}}},"404":{"description":"Offer not in the feed, or the endpoint is not implemented yet. Both are valid answers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/campaign-candidates":{"get":{"operationId":"campaign_candidates","summary":"Which offers should get a campaign, with what budget and for how long.","description":"This is what makes campaigns appear on their own. A candidate is your business decision — a paid visibility product, an activated PPC product, whatever you sell — and it carries the window and any budget override. Do not filter by whether the offer is published; we evaluate that ourselves from the detail endpoint. Cancellation is `status: \"cancelled\"` on the same pair, never a disappearance. The list doubles as reconciliation: if a push call is ever lost, the next walk fixes it by itself.\n\n**When this endpoint is missing:** No campaign is ever created automatically — every one has to be pushed to our API or created by hand.","x-upwolf-level":"recommended","parameters":[{"name":"updated_since","in":"query","required":false,"schema":{"type":"string"},"description":"Omitted on the first walk — then we want everything."},{"name":"page","in":"query","required":false,"schema":{"type":"string"},"description":"Value of `next_page` from the previous response."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignCandidates"},"example":{"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}}}},"404":{"description":"Offer not in the feed, or the endpoint is not implemented yet. Both are valid answers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/offers/{external_id}/applications-stats":{"get":{"operationId":"applications_stats","summary":"How many responses an offer collected since a given moment.","description":"Two numbers decide when we stop spending. `count` is the quality responses — how you define quality is entirely your business — and campaigns stop once it crosses the threshold. `total_count` is every application; it never drives a decision, it only makes the charts honest, because \"5 good ones\" reads very differently out of 6 than out of 60. The count may go down (withdrawn responses) and that is fine: our decisions never reverse, so a drop cannot restart a stopped campaign.\n\n**When this endpoint is missing:** Campaigns run for the full window and spend the whole budget even on offers that were filled long ago, and the guard that skips campaigns for offers filling up organically never fires.","x-upwolf-level":"recommended","parameters":[{"name":"external_id","in":"path","required":true,"schema":{"type":"string"},"description":"Stable offer id."},{"name":"from","in":"query","required":true,"schema":{"type":"string"},"description":"Count responses from this moment on."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationsStats"},"example":{"external_id":"DEMO-1001","from":"2026-09-01T00:00:00+02:00","count":4,"total_count":11}}}},"404":{"description":"Offer not in the feed, or the endpoint is not implemented yet. Both are valid answers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"OffersSnapshot":{"title":"Active offers snapshot","type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"external_id":{"type":"string","description":"Stable id of the offer. Never reused for a different offer."},"name":{"type":"string","description":"Offer title, so the listing renders without a detail call."},"company":{"type":"object","properties":{"name":{"type":"string","description":"Company name for the same reason."}},"required":[]}},"required":["external_id"]}}},"required":[]},"Offer":{"title":"Offer detail","type":"object","properties":{"external_id":{"type":"string","description":"Same id as in the snapshot."},"version":{"type":"string","description":"Opaque content fingerprint. Changes whenever the offer content changes."},"name":{"type":"string","description":"Offer title. Becomes the headline of the creative."},"description":{"type":"string","description":"Plain text or HTML. Everything the ad copy is written from — the richer, the better the ad."},"status":{"type":"string","description":"Only `published` offers get a running campaign; `closed` pauses it within minutes.","enum":["published","pending","closed"]},"updated_at":{"type":"string","description":"When the offer last changed."},"company":{"type":"object","properties":{"name":{"type":"string","description":"Advertiser name. Appears in the creative and prefixes the ad copy."},"logo_url":{"type":"string","description":"Publicly reachable HTTPS image. Square or wide, at least 200 px."},"introduction":{"type":"string","description":"Short company blurb; gives the ad copy something to work with beyond the offer itself."},"logo_approved":{"type":"boolean","description":"Set false if the logo is unverified; we then keep it out of rendered creative."}},"required":["name"]},"area_id":{"type":"string","description":"Your category id for the offer. Any stable identifier from your own taxonomy."},"area_root_id":{"type":"string","description":"Root of your category tree for the same offer."},"locations":{"type":"array","items":{"type":"object","properties":{"city":{"type":"string","description":"City as you write it in the listing. We map it to ad platform ids ourselves. Send the array even when empty — that tells us \"remote\", not \"not implemented\"."}},"required":[]}},"language_code":{"type":"string","description":"Language of the content. Without it we detect the language from the text."},"salaries":{"type":"array","items":{"type":"object","properties":{"minimum":{"type":"integer","description":"Lower bound of the offered amount, in whole units of the currency."},"maximum":{"type":"integer","description":"Upper bound, when you publish a range."},"currency":{"type":"string","description":"Currency of the amount, e.g. `CZK`."},"measure":{"type":"string","description":"Period the amount applies to.","enum":["hour","day","month","year","once"]}},"required":[]}}},"required":["external_id","version","name","description","status","updated_at"]},"OfferChanges":{"title":"Change list","type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"external_id":{"type":"string","description":"Which offer changed."},"version":{"type":"string","description":"Its current content fingerprint."},"status":{"type":"string","description":"Saves a detail call when all we need is the status.","enum":["published","pending","closed"]},"updated_at":{"type":"string","description":"When it changed."}},"required":["external_id","version"]}},"next_page":{"type":["string","integer","null"],"description":"Handed back verbatim as `page`. `null` ends the walk."}},"required":["next_page"]},"CampaignCandidates":{"title":"Campaign candidates","type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"source_type":{"type":"string","description":"Which kind of product produced the candidate.","enum":["visibility","ppc"]},"external_key":{"type":"string","description":"Id of that product purchase on your side. Opaque to us; a number is fine."},"offer_external_id":{"type":"string","description":"The offer this campaign is for."},"product":{"type":"string","description":"Product key agreed with us — it selects budget, objective and ad account."},"window":{"type":"object","properties":{"since":{"type":"string","description":"Campaign may not run before this."},"until":{"type":["string","null"],"description":"End of the window. May be null for `ppc`, where we derive it from the product configuration."}},"required":["since"]},"status":{"type":"string","description":"`cancelled` on the same pair stops the campaign. Never express cancellation by omission.","enum":["active","cancelled"]},"updated_at":{"type":"string","description":"Drives `updated_since` paging."},"budget_override":{"type":["integer","null"],"description":"Budget in minor units (hundredths). Overrides the product default."},"delay_override_days":{"type":["integer","null"],"description":"Overrides how long we wait before starting."},"first_campaign_boost":{"type":"boolean","description":"New client — start immediately, skip the waiting period. Default false."}},"required":["source_type","external_key","offer_external_id","product","status","updated_at"]}},"next_page":{"type":["string","integer","null"],"description":"Handed back verbatim as `page`. `null` ends the walk."}},"required":["next_page"]},"ApplicationsStats":{"title":"Response counts","type":"object","properties":{"external_id":{"type":"string","description":"Echo of the offer id."},"from":{"type":"string","description":"Echo of the requested lower bound. Handy when debugging, nothing depends on it."},"count":{"type":"integer","description":"Quality responses since `from`. Must be an integer — a numeric string is ignored."},"total_count":{"type":"integer","description":"All applications since `from`. Must be greater than or equal to `count`, otherwise we treat it as unknown."}},"required":["external_id","count"]},"Error":{"title":"Error","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Short machine-readable code."},"message":{"type":"string","description":"Human explanation. Ends up in our operator UI, so write it for a person."}},"required":["code","message"]}},"required":[]}}}}