{"openapi":"3.1.0","info":{"title":"FirstFlag API","version":"1.0.0","description":"Read your signals and keep your watchlists in step with the systems you already work out of.\n\nAuthenticate with `Authorization: Bearer ff_live_…`. Keys are created in Settings → API and are shown once.\n\nRate limit: 120 requests per minute per key.\n\n**Agencies.** An organization key (Settings → Connect your AI → Agency key) reaches every client workspace in your agency. Name the client with the `X-FirstFlag-Workspace` header or a `workspace` query parameter (its id or slug from `GET /workspaces`). Without one, `GET /signals` returns every client's signals (each tagged with `workspace`), and the signal routes find the signal's workspace themselves; watchlists and webhooks need a workspace. A workspace key keeps working exactly as before and is refused (403) if it names another workspace.","contact":{"name":"FirstFlag support","email":"support@firstflag.io"}},"servers":[{"url":"https://api.firstflag.io/v1","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"ff_live_…"}},"schemas":{"Signal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"signal_type":{"type":"string","enum":["funding","hiring","job_change","acquisition","public_filing","champion_movement","website_change","newsroom","social_listening","uspto","ad_activity","press_mention","competitor_engagement"]},"signal_label":{"type":"string","description":"Human-readable name of the play, e.g. \"Funding rounds\"."},"status":{"type":"string","enum":["ready","delivered","approved","dropped","exported"]},"detected_at":{"type":"string","format":"date-time"},"enriched_at":{"type":"string","format":"date-time","description":"When the contact was found. This is the field `since` and the cursor page on."},"company":{"type":"object","properties":{"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true}}},"contact":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":"string","nullable":true,"format":"email"},"email_verified_at":{"type":"string","nullable":true,"format":"date-time"},"phone":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true}}},"fit":{"type":"object","properties":{"score":{"type":"number","nullable":true},"reasons":{"type":"array","items":{"type":"string"}}}},"draft":{"type":"object","description":"A first-touch email written against this signal. Yours to send, edit or ignore.","properties":{"subject":{"type":"string","nullable":true},"body":{"type":"string","nullable":true},"opener":{"type":"string","nullable":true}}},"details":{"type":"object","description":"Play-specific facts: the round size, the job title posted, the page that changed.","additionalProperties":true}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}},"paths":{"/workspaces":{"get":{"summary":"List the client workspaces this key can reach","description":"An organization key lists every workspace in the agency; a workspace key lists just its own.","responses":{"200":{"description":"Workspaces (id, slug, name, website_url, plan)"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Add a client workspace from its website (organization keys)","description":"Reads the site, switches on the recommended plays and starts a 30-day backfill. Counts against the plan's workspace limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["website"],"properties":{"website":{"type":"string"},"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Workspace created; setup running"},"402":{"description":"Workspace limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"agency_plan_required (plan has no client workspaces) or org_key_required (Agency plan, workspace key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/me":{"get":{"summary":"Account, plan and current usage","description":"The call to make first when a key is not behaving as expected.","responses":{"200":{"description":"Account details"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/signals":{"get":{"summary":"List signals","description":"Returns unlocked signals oldest-first, so `since` plus the cursor gives you a stable incremental sync. Signals you have not unlocked are not returned — they have no contact attached.","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Only signals enriched at or after this ISO-8601 timestamp."},{"name":"signal_type","in":"query","schema":{"type":"string","enum":["funding","hiring","job_change","acquisition","public_filing","champion_movement","website_change","newsroom","social_listening","uspto","ad_activity","press_mention","competitor_engagement"]}},{"name":"status","in":"query","schema":{"type":"string","enum":["ready","delivered","approved","dropped","exported"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"`next_cursor` from the previous page."}],"responses":{"200":{"description":"A page of signals","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Bad query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited — see Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/signals/{id}":{"get":{"summary":"Fetch one signal","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The signal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Signal"}}}},"402":{"description":"Signal is not unlocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such signal on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/signals/{id}/approve":{"post":{"summary":"Mark a signal approved","description":"Use this once your sequencer has queued the outreach, so the app agrees with what actually happened.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The updated signal"},"409":{"description":"Not in an approvable status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/signals/{id}/drop":{"post":{"summary":"Drop a signal","description":"Refunds the credit if the signal was delivered less than 24 hours ago.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Dropped, with whether the credit came back"}}}},"/signals/{id}/wrong":{"post":{"summary":"Report a wrong signal (the guarantee)","description":"Wrong fact, wrong person, person no longer there, or a bounced email: the credit comes back plus 1 bonus signal. Capped per month and one credit per company; a reported bounce is credited once FirstFlag's bounce feed verifies it. Idempotent.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","enum":["wrong_fact","wrong_person","left_company","bounced"]},"note":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Logged, with credited / credit_refunded / bonus_credits and a message"},"402":{"description":"Never unlocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many reports this hour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks":{"get":{"summary":"List webhook endpoints","responses":{"200":{"description":"Endpoints and the available event types"}}},"post":{"summary":"Create a webhook endpoint","description":"Register a public https URL to receive events as they happen: signal.ready, signal.approved, signal.dropped. The response includes the signing secret exactly once. Deliveries carry an X-FirstFlag-Signature header (t=<unix>,v1=<hex HMAC-SHA256 of '<t>.<raw body>'>) and retry on failure for up to ~15 hours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["signal.ready","signal.approved","signal.dropped"]},"description":"Omit to receive all events."}}}}}},"responses":{"200":{"description":"The endpoint, with its signing secret (shown once)"},"400":{"description":"URL is not a deliverable public address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Endpoint limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}/test":{"post":{"summary":"Send a test event","description":"Delivers a canned, signed payload so a receiver and its signature check can be proven before any real signal exists.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Whether the receiver accepted it, with status code"}}}},"/webhooks/{id}":{"delete":{"summary":"Delete a webhook endpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"}}}},"/watchlists":{"get":{"summary":"List watchlist entries","parameters":[{"name":"kind","in":"query","required":true,"schema":{"type":"string","enum":["website","newsroom","linkedin_profile","uspto_term","ad_domain","linkedin_company"]}}],"responses":{"200":{"description":"Entries of that kind"}}},"post":{"summary":"Add watchlist entries","description":"Same parser and plan caps as the app. Entries already present are reported as duplicates rather than failing the call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","entries"],"properties":{"kind":{"type":"string","enum":["website","newsroom","linkedin_profile","uspto_term","ad_domain","linkedin_company"]},"entries":{"type":"array","maxItems":1000,"items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"value":{"type":"string"},"label":{"type":"string"}},"required":["value"]}]}}}}}}},"responses":{"200":{"description":"What was added"},"422":{"description":"Over your plan's cap for that list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}