openapi: 3.1.0 info: title: Silah Connect version: "1" summary: Connect a Silah workspace to your own website or app. description: | Silah Connect has two directions, signed the same way with the same secret. - **paths**: the Connect API your server calls on Silah (tickets, verification links). - **webhooks**: the endpoints on your site that Silah calls (manifest, resolve, tools, events), relative to the base URL entered in Silah. Every request in both directions carries `Silah-Site-Id`, `Silah-Timestamp`, `Silah-Nonce` and `Silah-Signature`. The signature is `v1=` + hex HMAC-SHA256(secret, canonical), where canonical is `v1\n{timestamp}\n{nonce}\n{METHOD}\n{path and query as sent}\n{hex sha256(body)}`. v1 only grows: new optional fields, capabilities, event types, endpoints and error codes may appear. Ignore what you do not know. Full guide: https://silah.chat/docs/connect license: name: Proprietary url: https://silah.chat/terms contact: name: Silah url: https://silah.chat/docs/connect externalDocs: description: Silah Connect guide url: https://silah.chat/docs/connect servers: - url: https://silah.chat/api/v1/connect/v1 security: - silahSignature: [] tags: - name: Connection description: Check that the Site ID and secret work. - name: Tickets description: Support tickets for your signed-in customers, answered in the Silah inbox. - name: Verifications description: Verification links the AI sends to customers on WhatsApp, Messenger and email. - name: Your site description: Endpoints your site serves under its base URL. Silah calls them. paths: /ping: get: tags: [Connection] operationId: ping summary: Check the credentials responses: "200": description: The credentials work. content: application/json: schema: type: object required: [silah_connect, site, workspace] properties: silah_connect: { type: string, const: "1" } site: type: object required: [id, name, status] properties: id: { type: string, description: The Site ID } name: { type: string } status: { type: string, enum: [active, paused] } workspace: type: object required: [name, slug] properties: name: { type: string } slug: { type: string } "4XX": { $ref: "#/components/responses/Error" } /tickets: get: tags: [Tickets] operationId: listTickets summary: List a customer's tickets description: The customer's own tickets plus every ticket opened with the same account, most recently active first. parameters: - { name: customer_subject, in: query, required: true, schema: { type: string, maxLength: 200 } } - { name: customer_account, in: query, schema: { type: string, maxLength: 200 } } - { name: status, in: query, schema: { $ref: "#/components/schemas/TicketStatusFilter" } } - { name: cursor, in: query, schema: { type: string } } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } } responses: "200": { $ref: "#/components/responses/TicketList" } "4XX": { $ref: "#/components/responses/Error" } post: tags: [Tickets] operationId: createTicket summary: Open a ticket description: Rate limited to 20 new tickets per customer per hour. requestBody: required: true content: application/json: schema: type: object required: [customer, subject, body] properties: customer: { $ref: "#/components/schemas/ConnectCustomer" } subject: { type: string, minLength: 1, maxLength: 200 } body: { type: string, minLength: 1, maxLength: 4000 } priority: { $ref: "#/components/schemas/TicketPriority" } responses: "201": { $ref: "#/components/responses/TicketDetail" } "4XX": { $ref: "#/components/responses/Error" } /tickets/query: post: tags: [Tickets] operationId: queryTickets summary: List a customer's tickets (fields in the body) description: The same as `GET /tickets`, for servers that keep customer ids out of URLs and access logs. requestBody: required: true content: application/json: schema: type: object required: [customer] properties: customer: { $ref: "#/components/schemas/ConnectCustomer" } status: { $ref: "#/components/schemas/TicketStatusFilter" } cursor: { type: string } limit: { type: integer, minimum: 1, maximum: 100, default: 25 } responses: "200": { $ref: "#/components/responses/TicketList" } "4XX": { $ref: "#/components/responses/Error" } /tickets/{ticket_id}: parameters: - $ref: "#/components/parameters/TicketID" get: tags: [Tickets] operationId: getTicket summary: Read a ticket and its messages description: 404 unless the ticket is on this site and is the customer's own or their account's. parameters: - { name: customer_subject, in: query, required: true, schema: { type: string, maxLength: 200 } } - { name: customer_account, in: query, schema: { type: string, maxLength: 200 } } - $ref: "#/components/parameters/Before" responses: "200": { $ref: "#/components/responses/TicketDetail" } "4XX": { $ref: "#/components/responses/Error" } /tickets/{ticket_id}/get: parameters: - $ref: "#/components/parameters/TicketID" post: tags: [Tickets] operationId: getTicketByBody summary: Read a ticket (customer in the body) parameters: - $ref: "#/components/parameters/Before" requestBody: required: true content: application/json: schema: type: object required: [customer] properties: customer: { $ref: "#/components/schemas/ConnectCustomer" } responses: "200": { $ref: "#/components/responses/TicketDetail" } "4XX": { $ref: "#/components/responses/Error" } /tickets/{ticket_id}/messages: parameters: - $ref: "#/components/parameters/TicketID" post: tags: [Tickets] operationId: replyToTicket summary: The customer replies description: Reopens a resolved ticket. Rate limited to 120 replies per customer per hour. requestBody: required: true content: application/json: schema: type: object required: [customer, body] properties: customer: { $ref: "#/components/schemas/ConnectCustomer" } body: { type: string, minLength: 1, maxLength: 4000 } responses: "201": { $ref: "#/components/responses/TicketDetail" } "4XX": { $ref: "#/components/responses/Error" } /tickets/{ticket_id}/status: parameters: - $ref: "#/components/parameters/TicketID" post: tags: [Tickets] operationId: setTicketStatus summary: The customer closes or reopens a ticket requestBody: required: true content: application/json: schema: type: object required: [customer, status] properties: customer: { $ref: "#/components/schemas/ConnectCustomer" } status: { type: string, enum: [resolved, open] } responses: "200": { $ref: "#/components/responses/TicketDetail" } "4XX": { $ref: "#/components/responses/Error" } /verifications/{verification_id}: parameters: - $ref: "#/components/parameters/VerificationID" get: tags: [Verifications] operationId: getVerification summary: Read a verification link description: Called by the page at your verify_url, with the id from its `request` query parameter. responses: "200": description: The verification. content: application/json: schema: { $ref: "#/components/schemas/Verification" } "4XX": { $ref: "#/components/responses/Error" } /verifications/{verification_id}/confirm: parameters: - $ref: "#/components/parameters/VerificationID" post: tags: [Verifications] operationId: confirmVerification summary: Confirm a verification for the signed-in customer description: Call it on a button press, never on page load. 409 `verification_closed` when the link was used, replaced or expired. requestBody: required: true content: application/json: schema: type: object required: [customer] properties: customer: { $ref: "#/components/schemas/ConnectCustomer" } responses: "200": description: Confirmed. The conversation is now verified and the AI carries on. content: application/json: schema: { $ref: "#/components/schemas/Verification" } "4XX": { $ref: "#/components/responses/Error" } webhooks: manifest: get: tags: [Your site] operationId: siteManifest summary: "GET {base}/manifest: describe the site" description: | Required. Fetched when a workspace admin tests the site. `proof` is hex HMAC-SHA256(secret, "silah-connect-proof\n" + the Silah-Nonce of this request). responses: "200": description: The manifest. content: application/json: schema: { $ref: "#/components/schemas/Manifest" } customersResolve: post: tags: [Your site] operationId: siteResolveCustomer summary: "POST {base}/customers/resolve: who is this customer" description: | Only with the `resolve` capability. At `claimed`, the email or phone is unproven: answer 404 or with nothing private. Answers, including 404, are cached for 5 minutes. requestBody: required: true content: application/json: schema: type: object required: [customer] properties: customer: { $ref: "#/components/schemas/SiteCustomer" } conversation: { $ref: "#/components/schemas/ConversationInfo" } responses: "200": description: What the AI may know about the customer. content: application/json: schema: type: object required: [customer] properties: customer: { $ref: "#/components/schemas/ResolvedCustomer" } "404": description: No such customer. toolCall: post: tags: [Your site] operationId: siteCallTool summary: "POST {base}/tools/{name}: run a tool for the AI" description: | Only with the `tools` capability, and only for tools the manifest declares, when the customer's level is at least the tool's `requires`. `{name}` is the tool's name from the manifest. Scope the lookup to `customer.subject` and `customer.account`. The AI reads up to 16 KB of the result; on failure it reads the error message. requestBody: required: true content: application/json: schema: type: object required: [input, customer] properties: input: type: object description: Chosen by the AI and shaped by the tool's input_schema. Validate it. customer: { $ref: "#/components/schemas/SiteCustomer" } conversation: { $ref: "#/components/schemas/ConversationInfo" } responses: "200": description: The tool's result. content: application/json: schema: type: object required: [result] properties: result: description: Any JSON. Facts, not prose. "4XX": { $ref: "#/components/responses/Error" } events: post: tags: [Your site] operationId: siteEvent summary: "POST {base}/events: something changed" description: | Only with the `events` capability. Retried after 5 s, 30 s and 2 min on a network error, 5xx or 429; a 4xx is final. The `id` is kept across retries. Answer 2xx to event types you do not handle. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/Event" } responses: "2XX": description: Received. components: securitySchemes: silahSignature: type: apiKey in: header name: Silah-Signature description: | `v1=` + hex HMAC-SHA256(secret, canonical), with `Silah-Site-Id`, `Silah-Timestamp` (Unix seconds, within 5 minutes) and `Silah-Nonce` (16-64 of A-Z a-z 0-9 _ -, never reused) on the same request. Several signatures may be sent separated by spaces or commas; unknown versions are ignored. parameters: TicketID: name: ticket_id in: path required: true schema: { type: string, format: uuid } VerificationID: name: verification_id in: path required: true schema: { type: string, format: uuid } Before: name: before in: query description: A message id; returns the 100 messages before it. schema: { type: string, format: uuid } responses: Error: description: An error. content: application/json: schema: { $ref: "#/components/schemas/Error" } TicketList: description: A page of tickets. content: application/json: schema: type: object required: [items, next_cursor] properties: items: type: array items: { $ref: "#/components/schemas/Ticket" } next_cursor: { type: [string, "null"] } TicketDetail: description: A ticket with its latest messages, oldest first. content: application/json: schema: type: object required: [ticket, messages, has_more] properties: ticket: { $ref: "#/components/schemas/Ticket" } messages: type: array maxItems: 100 items: { $ref: "#/components/schemas/TicketMessage" } has_more: { type: boolean } schemas: Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: | Connect codes: unsigned, stale_timestamp, bad_nonce, unknown_site, bad_signature, replayed (401); workspace_suspended, site_paused (403); not_found (404); verification_closed (409); too_large (413); rate_limited (429); validation_failed (400, with fields). message: { type: string } fields: type: object additionalProperties: { type: string } ConnectCustomer: type: object description: The signed-in customer your server is acting for. Your signature is the proof. required: [subject] properties: subject: { type: string, minLength: 1, maxLength: 200, description: Your stable id for the person } account: { type: string, maxLength: 200, description: A group of customers who share tickets } name: { type: string, maxLength: 200 } email: { type: string, format: email } phone: { type: string, description: "E.164, for example +8801711000111" } Level: type: string enum: [anonymous, claimed, channel, verified] description: | anonymous: nothing known. claimed: typed, unproven. channel: WhatsApp vouches for the phone. verified: your site or an issuer it trusts proved who they are. SiteCustomer: type: object description: Who a call to your site is about. subject and account are sent only at verified. required: [level] properties: level: { $ref: "#/components/schemas/Level" } subject: { type: string } account: { type: string } name: { type: string } email: { type: string } phone: { type: string } ConversationInfo: type: object required: [id, channel] properties: id: { type: string, format: uuid } channel: { type: string, examples: [web, whatsapp, messenger, instagram, email, site] } ResolvedCustomer: type: object required: [subject] properties: subject: { type: string } name: { type: string } email: { type: string } phone: { type: string } account: { type: string } summary: { type: string, description: The AI reads up to 1000 characters } facts: type: array description: The AI reads up to 30. items: type: object required: [label, value] properties: label: { type: string, maxLength: 80 } value: { type: string, maxLength: 500 } Tool: type: object required: [name, description] properties: name: type: string pattern: "^[a-z][a-z0-9_]{0,47}$" description: Unique; handoff_to_human and request_verification are reserved. description: { type: string, minLength: 1, maxLength: 1000, description: What the AI reads to decide when to use it } input_schema: type: object description: A JSON Schema with type object, at most 8 KB. Omit for no input. requires: type: string enum: [none, channel, verified] default: verified Manifest: type: object required: [silah_connect, name, capabilities, proof] properties: silah_connect: { type: string, const: "1" } name: { type: string, minLength: 1, maxLength: 80 } capabilities: type: array description: Unknown values are ignored. items: { type: string, examples: [resolve, tools, verify_link, events] } verify_url: type: string format: uri description: Required with verify_link. HTTPS; Silah appends request=. tools: type: array maxItems: 20 items: { $ref: "#/components/schemas/Tool" } proof: type: string description: hex HMAC-SHA256(secret, "silah-connect-proof\n" + Silah-Nonce of the request) Event: type: object required: [id, type, created_at, data] properties: id: { type: string, description: Kept across retries } type: { type: string, examples: [ticket.updated] } created_at: { type: string, format: date-time } data: oneOf: - $ref: "#/components/schemas/TicketUpdated" - type: object description: A later event type. TicketUpdated: type: object description: data of a ticket.updated event. required: [change, ticket] properties: change: { type: string, enum: [message, status] } ticket: type: object required: [id, number, status, customer] properties: id: { type: string, format: uuid } number: { type: [integer, "null"] } status: { $ref: "#/components/schemas/TicketStatus" } account: { type: [string, "null"] } customer: type: object required: [subject] properties: subject: { type: string } TicketStatus: type: string enum: [open, pending, resolved] TicketStatusFilter: type: string enum: [open, pending, resolved, all] default: all TicketPriority: type: string enum: [low, normal, high, urgent] default: normal Ticket: type: object required: [id, number, subject, status, priority, account, customer, waiting_on, last_message_at, created_at, updated_at] properties: id: { type: string, format: uuid } number: { type: [integer, "null"], description: "Shown as #1042 in the inbox" } subject: { type: string } status: { $ref: "#/components/schemas/TicketStatus" } priority: { $ref: "#/components/schemas/TicketPriority" } account: { type: [string, "null"] } customer: type: object required: [subject, name, email] properties: subject: { type: string } name: { type: [string, "null"] } email: { type: [string, "null"] } waiting_on: { type: string, enum: [team, customer] } last_message_at: { type: [string, "null"], format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } Attachment: type: object required: [id, name, content_type, size, kind, url] properties: id: { type: string } name: { type: string } content_type: { type: string } size: { type: integer } kind: { type: string, enum: [image, video, audio, file] } url: { type: string, format: uri } TicketMessage: type: object required: [id, from, author, body, attachments, created_at] properties: id: { type: string, format: uuid } from: { type: string, enum: [customer, team, ai] } author: type: [object, "null"] properties: name: { type: string } body: { type: string } attachments: type: array items: { $ref: "#/components/schemas/Attachment" } created_at: { type: string, format: date-time } Verification: type: object required: [id, status, channel, contact, expires_at, confirmed_at] properties: id: { type: string, format: uuid } status: { type: string, enum: [pending, confirmed, cancelled, expired] } channel: { type: string } contact: { type: string, description: "The masked phone or email the chat came from, e.g. +8801•••••••111" } expires_at: { type: string, format: date-time } confirmed_at: { type: [string, "null"], format: date-time }