{"openapi":"3.1.0","info":{"title":"Chamber Data API","version":"1.0.0","description":"Read and write access to a chamber's own data in Chamber Culture CRM, plus REST Hooks so an external system can subscribe itself to outbound webhook events. Every API key is scoped to exactly one chamber (tenant); there is no cross-tenant surface anywhere in this API.\n\nThis document covers ONLY the authenticated `\/api\/v1\/*` surface below. Chamber Culture CRM also has a separate, unauthenticated `\/api\/public\/*` surface (public directory\/events data for a chamber's own website) that is documented separately and is not described here.\n\n**Versioning.** The API is versioned by URL path segment, as a matter of convention: `\/api\/v1` today. There is no version header and no content negotiation. If a breaking change is ever needed, it ships as `\/api\/v2` while `\/api\/v1` keeps running unchanged - that is a stated engineering convention this team follows, not something enforced by the code itself, so it is a commitment rather than a contractual guarantee.\n\n**Rate limit.** 120 requests per minute per API key, applied to the whole `\/api\/v1` group, on top of the platform's standard abuse throttling.\n\nSee docs\/API.md in the application repository for prose documentation and worked examples; this file is the source of truth both are generated from \/ checked against."},"servers":[{"url":"https:\/\/crm.chamberculture.com"}],"tags":[{"name":"Members"},{"name":"Contacts"},{"name":"Events"},{"name":"Invoices"},{"name":"Payments"},{"name":"Deals"},{"name":"Connection"},{"name":"Webhooks"}],"security":[{"bearerAuth":[]}],"paths":{"\/api\/v1\/members":{"get":{"operationId":"api.v1.members.index","summary":"List members","tags":["Members"],"description":"Paginated list of this chamber's members, ordered by name by default.\n\nRequires scope: `members:read`.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","pending","lapsed","former"]}},{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["name","-name","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"post":{"operationId":"api.v1.members.store","summary":"Create a member","tags":["Members"],"description":"Creates a member. Arrives as whatever status was named explicitly - there is no default, so it is always a deliberate choice. Fires member.created.\n\nRequires scope: `members:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/members\/search":{"get":{"operationId":"api.v1.members.search","summary":"Find a member by email or name","tags":["Members"],"description":"The find half of Zapier's find-or-create. At least one of email\/name is required. email matches exactly (case-insensitive); name matches as a substring.\n\nRequires scope: `members:read`.","parameters":[{"name":"email","in":"query","required":false,"schema":{"type":"string","format":"email"}},{"name":"name","in":"query","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["name","-name","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/members\/{member}":{"get":{"operationId":"api.v1.members.show","summary":"Get one member","tags":["Members"],"description":"One member, same attributes shape as the list endpoint.\n\nRequires scope: `members:read`.","parameters":[{"name":"member","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"patch":{"operationId":"api.v1.members.update","summary":"Update a member","tags":["Members"],"description":"Partial update - omitted fields are left alone. Fires member.updated when anything actually changed.\n\nRequires scope: `members:write`.","parameters":[{"name":"member","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MemberResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/contacts":{"get":{"operationId":"api.v1.contacts.index","summary":"List contacts","tags":["Contacts"],"description":"Paginated list of this chamber's contacts, ordered by last name by default.\n\nRequires scope: `contacts:read`.","parameters":[{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["last_name","-last_name","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"post":{"operationId":"api.v1.contacts.store","summary":"Create a contact","tags":["Contacts"],"description":"Creates a contact.\n\nRequires scope: `contacts:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/contacts\/search":{"get":{"operationId":"api.v1.contacts.search","summary":"Find a contact by email","tags":["Contacts"],"description":"The find half of Zapier's find-or-create. Case-insensitive exact match.\n\nRequires scope: `contacts:read`.","parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}},{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["last_name","-last_name","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/contacts\/{contact}":{"patch":{"operationId":"api.v1.contacts.update","summary":"Update a contact","tags":["Contacts"],"description":"Partial update - omitted fields are left alone.\n\nRequires scope: `contacts:write`.","parameters":[{"name":"contact","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/subscribers":{"post":{"operationId":"api.v1.subscribers.store","summary":"Newsletter opt-in","tags":["Contacts"],"description":"Idempotent on email (case-insensitive): posting an address that already exists resubscribes that contact rather than erroring or duplicating. Lives under contacts:write because a subscriber IS a Contact.\n\nRequires scope: `contacts:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/SubscriberCreateRequest"}}}},"responses":{"200":{"description":"An existing, already-subscribed contact was matched.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactResponse"}}}},"201":{"description":"A new contact was created, or an unsubscribed contact was resubscribed.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ContactResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/events":{"get":{"operationId":"api.v1.events.index","summary":"List events","tags":["Events"],"description":"Paginated list of this chamber's events, newest-starting first by default.\n\nRequires scope: `events:read`.","parameters":[{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["starts_at","-starts_at","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EventListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"post":{"operationId":"api.v1.events.store","summary":"Create an event","tags":["Events"],"description":"Creates an event. Ticket types, the flyer image and QuickBooks class tagging stay UI-only in this version. Everything reaching this endpoint is an unattended automation, so it defaults to draft and non-public - pass status=\"published\" and is_public=true to opt into publishing immediately.\n\nRequires scope: `events:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EventCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EventResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/events\/{event}":{"patch":{"operationId":"api.v1.events.update","summary":"Update an event","tags":["Events"],"description":"Partial update. status may only move between draft and published - a cancelled or completed event keeps its terminal status regardless of what is sent.\n\nRequires scope: `events:write`.","parameters":[{"name":"event","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EventUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EventResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/events\/{event}\/registrations":{"get":{"operationId":"api.v1.events.registrations","summary":"List an event's registrations","tags":["Events"],"description":"Paginated, ordered by registered_at.\n\nRequires scope: `registrations:read`.","parameters":[{"name":"event","in":"path","required":true,"schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/EventRegistrationListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/invoices":{"get":{"operationId":"api.v1.invoices.index","summary":"List invoices","tags":["Invoices"],"description":"Paginated list of this chamber's invoices, newest-issued first by default. pay_token is deliberately never exposed - it authorizes payment on the invoice.\n\nRequires scope: `invoices:read`.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["draft","open","paid","void","uncollectible"]}},{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["issued_at","-issued_at","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/InvoiceListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"post":{"operationId":"api.v1.invoices.store","summary":"Create a draft invoice","tags":["Invoices"],"description":"Always creates a DRAFT - unissued and unsent. Line prices are in dollars (\"250.00\"), never cents.\n\nRequires scope: `invoices:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/InvoiceCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/InvoiceResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/invoices\/{invoice}\/issue":{"post":{"operationId":"api.v1.invoices.issue","summary":"Issue a draft invoice","tags":["Invoices"],"description":"Moves a draft invoice to issued: assigns its number and due date, and fires invoice.issued. A separate call from creation, deliberately - a draft is not sent to a member until this is called.\n\nRequires scope: `invoices:write`.","parameters":[{"name":"invoice","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/InvoiceResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/invoices\/{invoice}\/payments":{"post":{"operationId":"api.v1.invoices.payments.store","summary":"Record a payment against an invoice","tags":["Payments"],"description":"Applies in full to the invoice in the URL. amount is in dollars (\"350.00\"), never cents. Paying more than the outstanding balance, or using method=\"refund\", is rejected with a 422.\n\nRequires scope: `payments:write`.","parameters":[{"name":"invoice","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/PaymentCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/PaymentResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/payments":{"get":{"operationId":"api.v1.payments.index","summary":"List payments","tags":["Payments"],"description":"Paginated list of this chamber's payments, newest-received first by default.\n\nRequires scope: `payments:read`.","parameters":[{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["received_on","-received_on","created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/PaymentListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/deals":{"get":{"operationId":"api.v1.deals.index","summary":"List Hot Deals","tags":["Deals"],"description":"Paginated list of this chamber's Hot Deals, newest first by default.\n\nRequires scope: `deals:read`.","parameters":[{"name":"sort","in":"query","required":false,"description":"Sort column. Prefix with '-' for descending. An unrecognized value is ignored and the endpoint's default order runs instead.","schema":{"type":"string","enum":["created_at","-created_at"]}},{"name":"page","in":"query","required":false,"description":"1-indexed page number. Every list endpoint paginates 100 rows per page.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DealListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"post":{"operationId":"api.v1.deals.store","summary":"Create a Hot Deal","tags":["Deals"],"description":"Defaults to pending, awaiting staff approval. Pass status=\"approved\" to publish immediately, which also fires deal.approved.\n\nRequires scope: `deals:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DealCreateRequest"}}}},"responses":{"201":{"description":"Created","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DealResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/deals\/{deal}":{"patch":{"operationId":"api.v1.deals.update","summary":"Update a Hot Deal","tags":["Deals"],"description":"Partial update. A pending or rejected deal transitioning to approved fires deal.approved.\n\nRequires scope: `deals:write`.","parameters":[{"name":"deal","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DealUpdateRequest"}}}},"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/DealResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/me":{"get":{"operationId":"api.v1.me","summary":"Check the connection","tags":["Connection"],"description":"Confirms a key works and reports what it can do: which chamber it belongs to and its exact scope list. Requires no scope beyond a valid key - a key that cannot even confirm it is valid is impossible to debug, and this reveals nothing the holder does not already have.\n\nRequires no scope beyond a valid API key.","responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/MeResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/hooks":{"get":{"operationId":"api.v1.hooks.index","summary":"List webhook subscriptions","tags":["Webhooks"],"description":"All of this chamber's webhook subscriptions, human-created and API-created alike. There is no hooks:read scope - a webhook subscription is configuration, not chamber data, so this is gated on hooks:write like every other hooks endpoint.\n\nRequires scope: `hooks:write`.","responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/HookListResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}},"post":{"operationId":"api.v1.hooks.store","summary":"Subscribe to webhook events","tags":["Webhooks"],"description":"Registers a callback URL for one or more events. Re-posting the same URL updates it in place rather than stacking a second subscription. The signing secret is returned once, in this response only.\n\nRequires scope: `hooks:write`.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/HookCreateRequest"}}}},"responses":{"201":{"description":"Created (or updated, if the URL already had a subscription)","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/HookCreateResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"422":{"$ref":"#\/components\/responses\/ValidationFailed"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}},"\/api\/v1\/hooks\/{hook}":{"delete":{"operationId":"api.v1.hooks.destroy","summary":"Unsubscribe a webhook","tags":["Webhooks"],"description":"Deletes the subscription. This is the API's one and only delete endpoint - there is no way to delete chamber, member or financial data through this API. Deleting a webhook subscription is how an automation unsubscribes itself (Zapier calls this when a Zap is switched off).\n\nRequires scope: `hooks:write`.","parameters":[{"name":"hook","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/HookDeleteResponse"}}}},"401":{"$ref":"#\/components\/responses\/Unauthorized"},"403":{"$ref":"#\/components\/responses\/Forbidden"},"404":{"$ref":"#\/components\/responses\/NotFound"},"429":{"$ref":"#\/components\/responses\/TooManyRequests"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Send the API key as a bearer token: `Authorization: Bearer cck_...`. Mint one from Settings, Website, Developers (chamber_admin only). The plaintext key is shown exactly once, at creation."}},"schemas":{"PageMeta":{"type":"object","description":"Pagination metadata on every list endpoint. 100 rows per page, always.","properties":{"current_page":{"type":"integer"},"per_page":{"type":"integer","example":100},"total":{"type":"integer"},"last_page":{"type":"integer"}},"required":["current_page","per_page","total","last_page"]},"MemberAttributes":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","pending","lapsed","former"]},"industry":{"type":"string","nullable":true},"secondary_industries":{"type":"array","items":{"type":"string"},"nullable":true},"slogan":{"type":"string","nullable":true},"about":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"phone":{"type":"string","nullable":true},"address_line1":{"type":"string","nullable":true},"address_line2":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true},"member_since":{"type":"string","format":"date","nullable":true},"employee_count":{"type":"integer","nullable":true},"directory_visible":{"type":"boolean"}}},"MemberResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/MemberAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"MemberListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/MemberResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"MemberResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/MemberResource"}},"required":["data"]},"MemberCreateRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":255},"status":{"type":"string","enum":["active","pending","lapsed","former"],"description":"Must be passed explicitly on create - there is no server-side default, so it is always a deliberate decision."},"industry":{"type":"string","maxLength":255,"nullable":true},"secondary_industries":{"type":"array","items":{"type":"string","maxLength":255},"nullable":true},"website":{"type":"string","maxLength":255,"nullable":true},"email":{"type":"string","format":"email","maxLength":255,"nullable":true},"phone":{"type":"string","maxLength":50,"nullable":true},"address_line1":{"type":"string","maxLength":255,"nullable":true},"address_line2":{"type":"string","maxLength":255,"nullable":true},"city":{"type":"string","maxLength":255,"nullable":true},"state":{"type":"string","maxLength":50,"nullable":true},"zip":{"type":"string","maxLength":20,"nullable":true},"member_since":{"type":"string","format":"date","nullable":true},"employee_count":{"type":"integer","minimum":0,"maximum":1000000,"nullable":true},"slug":{"type":"string","maxLength":255,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","nullable":true,"description":"Unique per chamber (not globally). Auto-generated if omitted."},"slogan":{"type":"string","maxLength":160,"nullable":true},"about":{"type":"string","maxLength":5000,"nullable":true},"directory_visible":{"type":"boolean","nullable":true}},"required":["name","status"]},"MemberUpdateRequest":{"allOf":[{"$ref":"#\/components\/schemas\/MemberCreateRequest"}],"description":"Partial update - every field is optional. Omitted fields are left alone. Sending name or status is not required on PATCH, unlike POST.","required":[]},"ContactAttributes":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":"string","format":"email","nullable":true},"phone":{"type":"string","nullable":true},"title":{"type":"string","nullable":true}}},"ContactResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/ContactAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"ContactListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/ContactResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"ContactResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/ContactResource"}},"required":["data"]},"ContactCreateRequest":{"type":"object","properties":{"first_name":{"type":"string","maxLength":255},"last_name":{"type":"string","maxLength":255},"email":{"type":"string","format":"email","maxLength":255,"nullable":true},"phone":{"type":"string","maxLength":50,"nullable":true},"title":{"type":"string","maxLength":255,"nullable":true}},"required":["first_name","last_name"]},"ContactUpdateRequest":{"allOf":[{"$ref":"#\/components\/schemas\/ContactCreateRequest"}],"description":"Partial update - every field is optional. Omitted fields are left alone.","required":[]},"SubscriberCreateRequest":{"type":"object","description":"Newsletter opt-in. Idempotent on email (case-insensitive): posting an address that already exists resubscribes that contact instead of erroring or duplicating. first_name\/last_name are only set at creation - resubscribing an existing contact never overwrites what staff already have on file.","properties":{"email":{"type":"string","format":"email","maxLength":255},"first_name":{"type":"string","maxLength":255,"nullable":true},"last_name":{"type":"string","maxLength":255,"nullable":true}},"required":["email"]},"EventAttributes":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","published","cancelled","completed"]},"venue":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"starts_at":{"type":"string","format":"date-time","nullable":true},"ends_at":{"type":"string","format":"date-time","nullable":true},"capacity":{"type":"integer","nullable":true},"is_public":{"type":"boolean"}}},"EventResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/EventAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"EventListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/EventResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"EventResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/EventResource"}},"required":["data"]},"EventCreateRequest":{"type":"object","properties":{"title":{"type":"string","maxLength":255},"description":{"type":"string","maxLength":10000,"nullable":true},"venue":{"type":"string","maxLength":255,"nullable":true},"address":{"type":"string","maxLength":255,"nullable":true},"city":{"type":"string","maxLength":255,"nullable":true},"state":{"type":"string","maxLength":255,"nullable":true},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","nullable":true,"description":"Must be after starts_at."},"capacity":{"type":"integer","minimum":1,"nullable":true},"event_type":{"type":"string","enum":["chamber","member","community"],"nullable":true,"description":"Defaults to 'chamber' if omitted or unrecognized."},"is_public":{"type":"boolean","nullable":true,"description":"Defaults to false. Governs whether the event leaves the building onto the chamber's public site; independent of status."},"is_featured":{"type":"boolean","nullable":true},"status":{"type":"string","enum":["draft","published"],"nullable":true,"description":"Defaults to 'draft'. Only draft\/published are settable via the API; cancelled\/completed are terminal states the CRM sets on its own."}},"required":["title","starts_at"]},"EventUpdateRequest":{"type":"object","description":"Partial update - every field is optional. If event_type or starts_at is present it must be non-null; a cancelled\/completed event keeps its terminal status regardless of what status is sent.","properties":{"title":{"type":"string","maxLength":255},"description":{"type":"string","maxLength":10000,"nullable":true},"venue":{"type":"string","maxLength":255,"nullable":true},"address":{"type":"string","maxLength":255,"nullable":true},"city":{"type":"string","maxLength":255,"nullable":true},"state":{"type":"string","maxLength":255,"nullable":true},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","nullable":true,"description":"Must be after starts_at."},"capacity":{"type":"integer","minimum":1,"nullable":true},"event_type":{"type":"string","enum":["chamber","member","community"],"nullable":true,"description":"Defaults to 'chamber' if omitted or unrecognized."},"is_public":{"type":"boolean","nullable":true,"description":"Defaults to false. Governs whether the event leaves the building onto the chamber's public site; independent of status."},"is_featured":{"type":"boolean","nullable":true},"status":{"type":"string","enum":["draft","published"],"nullable":true,"description":"Defaults to 'draft'. Only draft\/published are settable via the API; cancelled\/completed are terminal states the CRM sets on its own."}}},"EventRegistrationAttributes":{"type":"object","properties":{"event_id":{"type":"integer"},"member_id":{"type":"integer","nullable":true},"contact_id":{"type":"integer","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"status":{"type":"string"},"source":{"type":"string","nullable":true},"price_cents":{"type":"integer","nullable":true},"guest_count":{"type":"integer","nullable":true},"registered_at":{"type":"string","format":"date-time","nullable":true},"checked_in_at":{"type":"string","format":"date-time","nullable":true}}},"EventRegistrationResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/EventRegistrationAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"EventRegistrationListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/EventRegistrationResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"EventRegistrationResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/EventRegistrationResource"}},"required":["data"]},"InvoiceAttributes":{"type":"object","properties":{"member_id":{"type":"integer"},"number":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","open","paid","void","uncollectible"]},"currency":{"type":"string"},"subtotal_cents":{"type":"integer"},"total_cents":{"type":"integer"},"amount_paid_cents":{"type":"integer"},"balance_cents":{"type":"integer"},"issued_at":{"type":"string","format":"date-time","nullable":true},"due_on":{"type":"string","format":"date","nullable":true},"voided_at":{"type":"string","format":"date-time","nullable":true},"memo":{"type":"string","nullable":true}}},"InvoiceResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/InvoiceAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"InvoiceListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/InvoiceResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"InvoiceResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/InvoiceResource"}},"required":["data"]},"InvoiceCreateRequest":{"type":"object","description":"Always creates a DRAFT - unissued, unsent, no number, no due date. Those are assigned by a separate call to \/invoices\/{invoice}\/issue.","properties":{"member_id":{"type":"integer","description":"Must belong to this chamber."},"lines":{"type":"array","minItems":1,"items":{"type":"object","properties":{"description":{"type":"string","maxLength":255},"quantity":{"type":"integer","minimum":1,"nullable":true,"description":"Defaults to 1."},"unit":{"type":"string","example":"250.00","description":"Price of one, in DOLLARS as a string (e.g. \"250.00\"), never cents. Must not be negative."}},"required":["description","unit"]}},"memo":{"type":"string","maxLength":2000,"nullable":true}},"required":["member_id","lines"]},"PaymentAttributes":{"type":"object","properties":{"member_id":{"type":"integer"},"method":{"type":"string","enum":["card","check","cash","ach","comp","refund","other"]},"gateway":{"type":"string","nullable":true},"amount_cents":{"type":"integer"},"received_on":{"type":"string","format":"date","nullable":true},"reference":{"type":"string","nullable":true}}},"PaymentResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/PaymentAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"PaymentListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/PaymentResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"PaymentResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/PaymentResource"}},"required":["data"]},"PaymentCreateRequest":{"type":"object","description":"Applies in FULL to the one invoice named in the URL - there is no way to split one payment across several invoices through this endpoint. Paying more than the outstanding balance is rejected with a 422 stating the balance.","properties":{"method":{"type":"string","enum":["card","check","cash","ach","comp","other"],"description":"Any CRM payment method except 'refund' - refunds are rejected outright and must be recorded from the invoice screen."},"amount":{"type":"string","example":"350.00","description":"In DOLLARS as a string, never cents. Must be greater than zero."},"received_on":{"type":"string","format":"date","nullable":true},"reference":{"type":"string","maxLength":255,"nullable":true},"notes":{"type":"string","maxLength":2000,"nullable":true}},"required":["method","amount"]},"DealAttributes":{"type":"object","properties":{"member_id":{"type":"integer"},"title":{"type":"string"},"slug":{"type":"string","nullable":true},"discount_label":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","approved","rejected"]},"audience":{"type":"string","enum":["public","members"]},"starts_at":{"type":"string","format":"date-time","nullable":true},"ends_at":{"type":"string","format":"date-time","nullable":true},"is_featured":{"type":"boolean"}}},"DealResource":{"type":"object","properties":{"id":{"type":"integer"},"attributes":{"$ref":"#\/components\/schemas\/DealAttributes"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","attributes","created_at","updated_at"]},"DealListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/DealResource"}},"meta":{"$ref":"#\/components\/schemas\/PageMeta"}},"required":["data","meta"]},"DealResponse":{"type":"object","properties":{"data":{"$ref":"#\/components\/schemas\/DealResource"}},"required":["data"]},"DealCreateRequest":{"type":"object","properties":{"member_id":{"type":"integer","description":"Must belong to this chamber."},"title":{"type":"string","maxLength":255},"discount_label":{"type":"string","maxLength":40,"nullable":true},"description":{"type":"string","maxLength":2000,"nullable":true},"terms":{"type":"string","maxLength":500,"nullable":true},"cta_url":{"type":"string","format":"uri","maxLength":255,"nullable":true,"description":"Must start with http:\/\/ or https:\/\/."},"starts_at":{"type":"string","format":"date-time","nullable":true},"ends_at":{"type":"string","format":"date-time","nullable":true,"description":"Must be after today."},"status":{"type":"string","enum":["pending","approved","rejected"],"nullable":true,"description":"Defaults to 'pending', awaiting staff approval. Passing 'approved' fires the deal.approved webhook immediately."},"audience":{"type":"string","enum":["public","members"],"nullable":true,"description":"Defaults to 'public'."},"is_featured":{"type":"boolean","nullable":true}},"required":["member_id","title"]},"DealUpdateRequest":{"allOf":[{"$ref":"#\/components\/schemas\/DealCreateRequest"}],"description":"Partial update - every field is optional. A pending\/rejected to approved transition fires deal.approved.","required":[]},"HookItem":{"type":"object","description":"A webhook subscription. Deliberately NOT the {id, attributes, ...} resource envelope - a subscription is configuration, not chamber data.","properties":{"id":{"type":"integer"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","url","events","active","created_at"]},"HookListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#\/components\/schemas\/HookItem"}}},"required":["data"],"description":"No meta - the full subscription list for this chamber is returned in one page."},"HookCreateRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":500,"description":"Must start with https:\/\/ - these carry member data and a plaintext callback would put it on the wire in clear."},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["member.created","member.updated","contact.created","invoice.issued","invoice.paid","event.registration.created","deal.approved"]}}},"required":["url","events"]},"HookCreateResponse":{"allOf":[{"$ref":"#\/components\/schemas\/HookItem"}],"type":"object","description":"Same shape as HookItem, plus the signing secret. The secret is returned ONCE, here - there is nowhere else a machine-created subscription could get it. Re-posting the same URL updates the existing subscription rather than creating a second one, and returns its (already-known) secret again.","properties":{"secret":{"type":"string"}},"required":["secret"]},"HookDeleteResponse":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"]},"MeResponse":{"type":"object","description":"Confirms a key works and reports what it can do. Requires no scope beyond a valid key.","properties":{"chamber":{"type":"object","properties":{"id":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"website":{"type":"string","nullable":true}}},"key":{"type":"object","properties":{"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"can_write":{"type":"boolean"}}},"label":{"type":"string"}}},"ValidationErrorResponse":{"type":"object","description":"Standard Laravel validation failure. Every write endpoint that fails request validation returns this shape.","properties":{"message":{"type":"string"},"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["message","errors"]},"BusinessRuleErrorResponse":{"type":"object","description":"A hand-built 422 with no errors object, returned only by POST \/invoices\/{invoice}\/issue and POST \/invoices\/{invoice}\/payments when the request is well-formed but violates a business rule (e.g. issuing a non-draft invoice, or a payment that overpays the balance).","properties":{"message":{"type":"string"}},"required":["message"]},"ErrorResponse":{"type":"object","description":"Generic {message} error body used for 401, 403, 404 and 429.","properties":{"message":{"type":"string"}},"required":["message"]}},"responses":{"Unauthorized":{"description":"Missing, malformed or revoked API key. No further detail is given, deliberately - a vague 401 never confirms whether a prefix \"almost\" matched.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"Forbidden":{"description":"The key is valid but lacks the required scope. The message names the missing scope by design - the caller already holds the key and can see its scopes in Settings, so a vague error here just costs a support round trip.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"NotFound":{"description":"No row with that id in this chamber's own data. A row belonging to another chamber 404s the same way a missing id does - the tenant scope never reveals which.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}},"ValidationFailed":{"description":"Request validation failed. See ValidationErrorResponse and BusinessRuleErrorResponse - this API returns TWO different 422 shapes depending on the endpoint.","content":{"application\/json":{"schema":{"oneOf":[{"$ref":"#\/components\/schemas\/ValidationErrorResponse"},{"$ref":"#\/components\/schemas\/BusinessRuleErrorResponse"}]}}}},"TooManyRequests":{"description":"Rate limited - 120 requests per minute per API key, on top of the platform's standard abuse throttling. Back off; do not retry immediately.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/ErrorResponse"}}}}}}}