{"openapi":"3.1.0","info":{"title":"Scopable External CRM API","version":"1.0.0","summary":"Stable API-key facade for tenant-scoped clients and contacts.","description":"External API v1 lets tenant-owned integrations read and write clients and contacts without exposing internal browser APIs. Every data route derives tenant identity from the bearer API key and returns the standard public error envelope on failure."},"servers":[{"url":"https://api.scopable.io/functions/v1/public-api","description":"Production custom domain for External API v1."},{"url":"https://rhoirtnlknixidwispwt.supabase.co/functions/v1/public-api","description":"Direct Supabase Edge Function URL for diagnostics."}],"tags":[{"name":"System","description":"Key validation and machine-readable API metadata."},{"name":"Contacts","description":"Tenant-scoped client contact facade."},{"name":"Clients","description":"Tenant-scoped client organization facade."}],"security":[{"ApiKeyBearer":[]}],"components":{"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"Scopable API key","description":"Use `Authorization: Bearer sk_live_...` or `sk_test_...`."}},"parameters":{"Limit":{"name":"limit","in":"query","required":false,"description":"Page size from 1 to 100. Defaults to 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},"Cursor":{"name":"cursor","in":"query","required":false,"description":"Opaque cursor returned as `next_cursor` from the previous page.","schema":{"type":"string"}},"UpdatedSince":{"name":"updated_since","in":"query","required":false,"description":"Return rows updated at or after this ISO-8601 timestamp.","schema":{"type":"string","format":"date-time"}}},"responses":{"unauthorized":{"description":"401 unauthorized","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"forbidden":{"description":"403 forbidden","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"not_found":{"description":"404 not_found","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"invalid_request":{"description":"400 invalid_request","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"method_not_allowed":{"description":"405 method_not_allowed","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"rate_limited":{"description":"429 rate_limited","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"internal_error":{"description":"500 internal_error","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"schemas":{"PingResponse":{"$ref":"#/definitions/PingResponse"},"ErrorEnvelope":{"$ref":"#/definitions/ErrorEnvelope"},"Contact":{"$ref":"#/definitions/Contact"},"ContactWrite":{"$ref":"#/definitions/ContactWrite"},"ContactListQuery":{"$ref":"#/definitions/ContactListQuery"},"ContactCreate":{"$ref":"#/definitions/ContactCreate","additionalProperties":true},"ContactUpdate":{"$ref":"#/definitions/ContactUpdate"},"Client":{"$ref":"#/definitions/Client"},"ClientWrite":{"$ref":"#/definitions/ClientWrite"},"ClientListQuery":{"$ref":"#/definitions/ClientListQuery"},"ClientCreate":{"$ref":"#/definitions/ClientCreate","additionalProperties":true},"ClientUpdate":{"$ref":"#/definitions/ClientUpdate"},"ContactListPage":{"type":"object","required":["data","next_cursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"next_cursor":{"type":"string","nullable":true}},"additionalProperties":false},"ClientListPage":{"type":"object","required":["data","next_cursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Client"}},"next_cursor":{"type":"string","nullable":true}},"additionalProperties":false}}},"paths":{"/v1/openapi.json":{"get":{"summary":"Fetch the OpenAPI document","description":"Returns the machine-readable OpenAPI 3.1 contract. This route does not require an API key.","tags":["System"],"operationId":"getOpenApiDocument","security":[],"parameters":[],"responses":{"200":{"description":"OpenAPI 3.1 document","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"405":{"$ref":"#/components/responses/method_not_allowed"},"500":{"$ref":"#/components/responses/internal_error"}}}},"/v1/ping":{"get":{"summary":"Validate an API key","description":"Returns the tenant, key id, scopes, and environment derived from the bearer API key.","tags":["System"],"operationId":"ping","security":[{"ApiKeyBearer":[]}],"parameters":[],"responses":{"200":{"description":"API-key context","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingResponse"}}}},"401":{"$ref":"#/components/responses/unauthorized"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}}},"/v1/contacts":{"get":{"summary":"List contacts","description":"Returns tenant-scoped contacts as stable public DTOs. Results are cursor-paginated by id.","tags":["Contacts"],"operationId":"listContacts","security":[{"ApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/UpdatedSince"},{"name":"email","in":"query","required":false,"description":"Exact case-insensitive contact email filter.","schema":{"type":"string","format":"email"}},{"name":"client_id","in":"query","required":false,"description":"Only contacts attached to this client id.","schema":{"type":"string","format":"uuid"}}],"x-scopable-action-id":"public.contact.list","x-scopable-availability":"available","x-scopable-dto-input":null,"x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"query"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["contacts:read"],"x-scopable-rest-projection-path":"/v1/contacts","responses":{"200":{"description":"Contact page","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactListPage"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}},"post":{"summary":"Create or resolve a contact","description":"Validates the request, preserves the full inbound body in the source facet, and returns the resolved canonical contact plus source id.","tags":["Contacts"],"operationId":"createContact","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Caller-stable replay key used when `external_id` is omitted.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Contact create payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreate"}}}},"x-scopable-action-id":"public.contact.create","x-scopable-availability":"available","x-scopable-dto-input":"public.contact.create.input","x-scopable-idempotency":{"strategy":"resource_facet","facet":"contact_source"},"x-scopable-idempotency-header":"Idempotency-Key","x-scopable-required-capabilities":["create_contacts"],"x-scopable-required-scopes":["contacts:write"],"x-scopable-rest-projection-path":"/v1/contacts","responses":{"201":{"description":"Resolved contact","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactWrite"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}}},"/v1/contacts/{id}":{"get":{"summary":"Get a contact","description":"Returns one tenant-scoped contact. Missing and cross-tenant ids both return `not_found`.","tags":["Contacts"],"operationId":"getContact","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Canonical contact id returned by the API.","schema":{"type":"string","format":"uuid"}}],"x-scopable-action-id":"public.contact.get","x-scopable-availability":"available","x-scopable-dto-input":null,"x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"contact_id"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["contacts:read"],"x-scopable-rest-projection-path":"/v1/contacts/:id","responses":{"200":{"description":"Contact","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}},"patch":{"summary":"Update a contact","description":"Updates allowlisted public fields and records API-key provenance through a source facet.","tags":["Contacts"],"operationId":"updateContact","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Canonical contact id returned by the API.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"Contact update payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"}}}},"x-scopable-action-id":"public.contact.update","x-scopable-availability":"available","x-scopable-dto-input":"public.contact.update.input","x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"contact_id"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["create_contacts"],"x-scopable-required-scopes":["contacts:write"],"x-scopable-rest-projection-path":"/v1/contacts/:id","responses":{"200":{"description":"Updated contact","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactWrite"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}},"delete":{"summary":"Delete a contact","description":"Deletes a tenant-scoped contact while preserving source-facet provenance.","tags":["Contacts"],"operationId":"deleteContact","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Canonical contact id returned by the API.","schema":{"type":"string","format":"uuid"}}],"x-scopable-action-id":"public.contact.delete","x-scopable-availability":"available","x-scopable-dto-input":null,"x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"contact_id"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["create_contacts"],"x-scopable-required-scopes":["contacts:write"],"x-scopable-rest-projection-path":"/v1/contacts/:id","responses":{"204":{"description":"Contact deleted","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}}},"/v1/clients":{"get":{"summary":"List clients","description":"Returns active tenant-scoped clients as stable public DTOs. Results are cursor-paginated by id.","tags":["Clients"],"operationId":"listClients","security":[{"ApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/UpdatedSince"},{"name":"email","in":"query","required":false,"description":"Exact case-insensitive filter on the client's contact_email.","schema":{"type":"string","format":"email"}}],"x-scopable-action-id":"public.client.list","x-scopable-availability":"available","x-scopable-dto-input":null,"x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"query"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["clients:read"],"x-scopable-rest-projection-path":"/v1/clients","responses":{"200":{"description":"Client page","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientListPage"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}},"post":{"summary":"Create or resolve a client","description":"Validates the request, preserves the full inbound body in the source facet, and returns the resolved canonical client plus source id.","tags":["Clients"],"operationId":"createClient","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Caller-stable replay key used when `external_id` is omitted.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Client create payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientCreate"}}}},"x-scopable-action-id":"public.client.create","x-scopable-availability":"available","x-scopable-dto-input":"public.client.create.input","x-scopable-idempotency":{"strategy":"resource_facet","facet":"client_source"},"x-scopable-idempotency-header":"Idempotency-Key","x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["clients:write"],"x-scopable-rest-projection-path":"/v1/clients","responses":{"201":{"description":"Resolved client","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientWrite"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}}},"/v1/clients/{id}":{"get":{"summary":"Get a client","description":"Returns one active tenant-scoped client. Missing, archived, and cross-tenant ids all return `not_found`.","tags":["Clients"],"operationId":"getClient","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Canonical client id returned by the API.","schema":{"type":"string","format":"uuid"}}],"x-scopable-action-id":"public.client.get","x-scopable-availability":"available","x-scopable-dto-input":null,"x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"client_id"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["clients:read"],"x-scopable-rest-projection-path":"/v1/clients/:id","responses":{"200":{"description":"Client","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Client"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}},"patch":{"summary":"Update a client","description":"Updates allowlisted public fields and records API-key provenance through a source facet.","tags":["Clients"],"operationId":"updateClient","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Canonical client id returned by the API.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"description":"Client update payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientUpdate"}}}},"x-scopable-action-id":"public.client.update","x-scopable-availability":"available","x-scopable-dto-input":"public.client.update.input","x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"client_id"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["clients:write"],"x-scopable-rest-projection-path":"/v1/clients/:id","responses":{"200":{"description":"Updated client","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientWrite"}}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}},"delete":{"summary":"Archive a client","description":"Archives an active tenant-scoped client while preserving its canonical record and source-facet provenance.","tags":["Clients"],"operationId":"deleteClient","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Canonical client id returned by the API.","schema":{"type":"string","format":"uuid"}}],"x-scopable-action-id":"public.client.delete","x-scopable-availability":"available","x-scopable-dto-input":null,"x-scopable-idempotency":{"strategy":"deterministic_effect","stateKey":"client_id"},"x-scopable-idempotency-header":null,"x-scopable-required-capabilities":["view_crm"],"x-scopable-required-scopes":["clients:write"],"x-scopable-rest-projection-path":"/v1/clients/:id","responses":{"204":{"description":"Client archived","headers":{"X-Request-Id":{"schema":{"type":"string"},"description":"Request correlation id for support and audit lookup."}}},"400":{"$ref":"#/components/responses/invalid_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"429":{"$ref":"#/components/responses/rate_limited"},"500":{"$ref":"#/components/responses/internal_error"}}}}}}