--- published: true layout: post title: 'Overlays That Speak: Conversational Phrasing For Every Operation' image: https://kinlane-images.s3.amazonaws.com/apievangelist/api-evangelist-images/overlays-that-speak-conversational-phrasing-for-every-operation.png date: 2026-09-24 author: Kin Lane tags: - OpenAPI - Overlays - Agents - LLMs - Discovery - APIs.io - APIs --- People do not ask their LLMs for `POST /v1/refunds`. They ask "can I give a partial refund instead of refunding the whole charge?" And when they hand the work to an agent they do not say `PostRefunds`, they say "refund $20 of that payment." Every operation we have indexed has a path, a method, a summary and a pile of parameters. None of them have the words people actually use when they go looking for what that operation does. So tonight I added those words. For ten providers — Stripe, Twilio, HubSpot, Plaid, GitHub, Shopify, Slack, Adyen, SendGrid and Zoom — every operation now carries the questions someone would ask an LLM about it and the instructions they would give an agent to run it. That is 6,399 operations across 607 APIs, and all of it lives in [OpenAPI Overlays](https://spec.openapis.org/overlay/latest.html), not in the provider's contract. ## Two registers, one extension Each operation gets an `x-apievangelist-phrasing` block with four things: - **An intent** — a short, verb-first label in business language. "Refund a payment," not `PostRefunds`. - **Questions** — two to four things a developer, operator or business user asks an LLM while exploring or deciding. They end in a question mark and carry no placeholders. - **Instructions** — two to four things someone tells an agent to *do*. Every value the person would supply is a `{slot}`, and every slot is mapped to a real parameter of that operation: `path.pull_number`, `query.limit`, `requestBody.amount`. - **An effect** — `read`, `write` or `destructive`. That is how an agent knows when to stop and confirm before acting. It goes in its own overlay per API, `overlays/-phrasing-overlay.yaml`, alongside the enhancement overlays we already maintain, so it can be regenerated without touching anything else. Overlays are exactly the right tool for this. The contract stays what the provider published; the phrasing is a layer we apply on top, and anyone can read, diff, or throw it away. ## The part that keeps it honest The phrasing is written by a model. Everything else is a script. The model only drafts, and a validator decides what ships: - Every `{slot}` in an instruction has to map to a parameter that actually exists on that operation. If Stripe's refund had no `amount` parameter, there would be no "partial refund" phrasing, because the validator would reject it. That is the never-fabricate rule made mechanical. - Every required path parameter has to show up as a slot somewhere, so an agent knows what to ask for. "Create an onboarding link for a connected account" fails. "Create an onboarding link for `{account}`" passes. - No two operations in an API can share a phrase, or even come within 85% of one. A phrase that fits two siblings routes to neither. - Deletes are always destructive. A search that happens to be a `POST` is a read. And a `GET` that changes state — Slack declares `auth.revoke` and `apps.uninstall` as GETs — can be marked destructive, because an agent that doesn't pause before revoking a token is a problem. Then I tested whether the phrasing actually works. For each provider a separate model was handed only the phrases and each API's list of operations, with the answers hidden, and asked to pick which operation each phrase belongs to. It picked the right one **2,991 times out of 3,041 — 98.4%**. Slack came in at 100%. Plaid, with all 265 operations behind a single menu, was the hardest at 94.3%. Most of the misses were contracts that describe the same endpoint twice, which is a finding about the contract, not the phrasing. ## Labeled for what it is This is our phrasing, not observed demand. Every block says `method: generated`, it never counts toward a Kin Score, and when it shows up on APIs.io it carries a visible "Generated by API Evangelist" label. Down the road I want an `observed` layer — phrases matched from what people really type into APIs.io search and what agents really ask for — and that has to stay separate from what we wrote. On APIs.io, each operation on an API page gets its intent, an "Ask an LLM" column and a "Tell an agent" column, with a confirm-first flag on anything destructive. Each provider page gets a "What people ask" section that rolls the intents up across all of its APIs, each one linked straight to the operation that does it. It lands with the next build. ## One from each of the ten Here is a real block from each provider, straight out of the overlays committed tonight. **Stripe** — the partial refund is only offered because `amount` is a real parameter: ```yaml - target: $.paths['/v1/refunds'].post update: x-apievangelist-phrasing: intent: Refund a payment effect: write questions: - How do I refund a customer's payment? - Can I give a partial refund instead of refunding the whole charge? - Can I record why I'm refunding, like a duplicate or fraudulent charge? instructions: - text: Refund payment {payment_intent} in full. slots: payment_intent: requestBody.payment_intent - text: Refund {amount} of payment {payment_intent}. slots: amount: requestBody.amount payment_intent: requestBody.payment_intent - text: Refund charge {charge} with reason {reason}. slots: charge: requestBody.charge reason: requestBody.reason method: generated generated: '2026-09-24' ``` **Twilio** — one operation, four distinct jobs, each built only from parameters Twilio actually takes: ```yaml - target: $.paths['/Accounts/{AccountSid}/Messages.json'].post update: x-apievangelist-phrasing: intent: Send an SMS or MMS message effect: write questions: - How do I send a text message to a customer's phone number with Twilio? - Can I attach an image to a text and send it as MMS? - Is it possible to schedule a text to go out later instead of right now? - Can I send a message using a content template and fill in its variables? instructions: - text: 'Text {to} from {from} on account {account}: {body}' slots: to: requestBody.To from: requestBody.From account: path.AccountSid body: requestBody.Body - text: Send an MMS to {to} with the image at {media_url} from account {account}. slots: to: requestBody.To media_url: requestBody.MediaUrl account: path.AccountSid - text: Schedule {body} to {to} at {send_at} through messaging service {service} on account {account}. slots: body: requestBody.Body to: requestBody.To send_at: requestBody.SendAt service: requestBody.MessagingServiceSid account: path.AccountSid - text: Send content template {content} to {to} with variables {variables} from account {account}. slots: content: requestBody.ContentSid to: requestBody.To variables: requestBody.ContentVariables account: path.AccountSid method: generated generated: '2026-09-24' ``` **HubSpot** — every input the enrollment needs is a slot, so an agent knows exactly what to ask for: ```yaml - target: $.paths['/automation/sequences/2026-03/enrollments'].post update: x-apievangelist-phrasing: intent: Enroll a contact in a sales sequence effect: write questions: - How do I enroll a contact into a sales sequence via the API? - Can I choose which sender email a sequence enrollment goes out from? instructions: - text: Enroll contact {contactId} in sequence {sequenceId} sending from {senderEmail} as user {userId}. slots: contactId: requestBody.contactId sequenceId: requestBody.sequenceId senderEmail: requestBody.senderEmail userId: query.userId - text: Put contact {contactId} into sequence {sequenceId}, sent by {senderEmail} via alias {senderAliasAddress}, on behalf of user {userId}. slots: contactId: requestBody.contactId sequenceId: requestBody.sequenceId senderEmail: requestBody.senderEmail senderAliasAddress: requestBody.senderAliasAddress userId: query.userId method: generated generated: '2026-09-24' ``` **Plaid** — a `POST` that only reads, so it is marked `read`, and the three instructions separate the first pull, resuming from a cursor, and a capped page: ```yaml - target: $.paths['/transactions/sync'].post update: x-apievangelist-phrasing: intent: Sync incremental transaction updates effect: read questions: - What's the recommended way to keep a user's transactions up to date? - How do I get only the transactions added, modified or removed since my last pull? - How many transaction updates can I get per sync page? instructions: - text: Sync transactions for {access_token} starting from cursor {cursor}. slots: access_token: requestBody.access_token cursor: requestBody.cursor - text: Pull the first full batch of transaction changes for item {access_token}. slots: access_token: requestBody.access_token - text: Get up to {count} transaction updates for {access_token} after cursor {cursor}. slots: count: requestBody.count access_token: requestBody.access_token cursor: requestBody.cursor method: generated generated: '2026-09-24' ``` **GitHub** — the third instruction uses the real `sha` guard, so the merge only happens if the branch hasn't moved since review: ```yaml - target: $.paths['/repos/{owner}/{repo}/pulls/{pull_number}/merge'].put update: x-apievangelist-phrasing: intent: Merge a pull request effect: write questions: - How do I merge a pull request through the API? - Can I squash or rebase instead of making a merge commit? - Is there a way to make the merge fail if the branch head moved since I reviewed it? instructions: - text: 'Merge PR #{pull_number} in {owner}/{repo}.' slots: pull_number: path.pull_number owner: path.owner repo: path.repo - text: Merge pull request {pull_number} in {owner}/{repo} using the {merge_method} method. slots: pull_number: path.pull_number owner: path.owner repo: path.repo merge_method: requestBody.merge_method - text: Merge PR {pull_number} in {owner}/{repo} only if its head is still {sha}, with commit title {commit_title}. slots: pull_number: path.pull_number owner: path.owner repo: path.repo sha: requestBody.sha commit_title: requestBody.commit_title method: generated generated: '2026-09-24' ``` **Shopify** — written the way a merchant thinks about it, "you may also like," not the way the endpoint is named: ```yaml - target: $.paths['/recommendations/products.json'].get update: x-apievangelist-phrasing: intent: Get recommended products for a product effect: read questions: - How can I show 'you may also like' products on a product page? - Can I get complementary products instead of related ones, and cap how many come back? - What's the most recommendations I can request for one product? instructions: - text: Get product recommendations for product {product_id}. slots: product_id: query.product_id - text: Show {limit} {intent} recommendations for product {product_id}. slots: limit: query.limit intent: query.intent product_id: query.product_id method: generated generated: '2026-09-24' ``` **Slack** — "right now" is doing real work in the last instruction; it is what separates this from `chat.scheduleMessage`: ```yaml - target: $.paths['/chat.postMessage'].post update: x-apievangelist-phrasing: intent: Send a message to a channel effect: write questions: - How do I post a message to a Slack channel from my app? - Can I reply in a thread and also broadcast the reply to the channel? - Can I send rich Block Kit layouts instead of plain text? instructions: - text: Post {text} to channel {channel}. slots: text: requestBody.text channel: requestBody.channel - text: Reply {text} in thread {thread_ts} of {channel}. slots: text: requestBody.text thread_ts: requestBody.thread_ts channel: requestBody.channel - text: Send the Block Kit message {blocks} to {channel} right now. slots: blocks: requestBody.blocks channel: requestBody.channel method: generated generated: '2026-09-24' ``` **Adyen** — a reversal refunds or cancels depending on whether the payment was captured, so it is `destructive` and phrased around the question merchants actually have: ```yaml - target: $.paths['/payments/{paymentPspReference}/reversals'].post update: x-apievangelist-phrasing: intent: Reverse a payment whether captured or not effect: destructive questions: - What if I don't know whether a payment was captured and just want the money back to the shopper? - Does a reversal cancel an uncaptured payment instead of refunding it? instructions: - text: Reverse payment {paymentPspReference} on merchant {merchantAccount}. slots: paymentPspReference: path.paymentPspReference merchantAccount: requestBody.merchantAccount - text: Refund or cancel {paymentPspReference} in full for {merchantAccount} with reference {reference}. slots: paymentPspReference: path.paymentPspReference merchantAccount: requestBody.merchantAccount reference: requestBody.reference method: generated generated: '2026-09-24' ``` **SendGrid** — a plain send, a dynamic-template send, and a scheduled send in a batch, each slotted to real request-body fields: ```yaml - target: $.paths['/v3/mail/send'].post update: x-apievangelist-phrasing: intent: Send an email effect: write questions: - How do I send a transactional email through the v3 Web API? - Can I send with a dynamic template and per-recipient personalizations? - Is it possible to schedule an email for later with a send time? instructions: - text: Send an email from {from} with subject {subject} to {personalizations}. slots: from: requestBody.from subject: requestBody.subject personalizations: requestBody.personalizations - text: Send template {template_id} from {from} to {personalizations}. slots: template_id: requestBody.template_id from: requestBody.from personalizations: requestBody.personalizations - text: Schedule mail from {from} to {personalizations} for {send_at} in batch {batch_id}. slots: from: requestBody.from personalizations: requestBody.personalizations send_at: requestBody.send_at batch_id: requestBody.batch_id method: generated generated: '2026-09-24' ``` **Zoom** — the limit of 30 comes from the contract, and it is exactly what someone planning a bulk registration wants to know: ```yaml - target: $.paths['/meetings/{meetingId}/batch_registrants'].post update: x-apievangelist-phrasing: intent: Register up to 30 people for a meeting at once effect: write questions: - Can I bulk-register a list of attendees for a meeting? - What's the maximum number of registrants I can add in one batch? instructions: - text: Batch register {registrants} for meeting {meetingId}. slots: registrants: requestBody.registrants meetingId: path.meetingId - text: Bulk-add up to 30 registrants {registrants} to meeting {meetingId} with auto-approve {auto_approve}. slots: registrants: requestBody.registrants meetingId: path.meetingId auto_approve: requestBody.auto_approve method: generated generated: '2026-09-24' ``` ## What the contracts told me along the way Writing a sentence about every operation is a very thorough way to read a contract, and the contracts had plenty to say. The drafting logged 88 defects across the ten providers: write operations with no request body, so there is nothing to put the email address or tracking number in; `PATCH` operations that mark every field required; descriptions copied from a sibling operation; summaries that are just the path repeated back; operationIds with spaces in them; the same operationId used twice in one contract; state-changing calls declared as `GET`; and two whole contract sets — Shopify's, which repeats every endpoint once per API version from 2020 and 2021, and GitHub's, which turns out to be Enterprise Server 3.9 rather than github.com — that need to be harvested again. Those go into the schema healing pass next. Operation IDs with whitespace or duplicates are now repaired as part of how we refine every OpenAPI we crawl. The bigger point is this. An API that can't be described in the words people use doesn't get found by the LLMs people now search with, and doesn't get used correctly by the agents they hand the work to. The contract says what an operation *is*. The phrasing says what it's *for*. Ten providers down, a lot more to go.