Resandhi

Add your own connector

This guide is for developers working in the Resandhi codebase. It explains how to add a new payment gateway, messaging service, email provider or product source, so that every shop can connect it in one click.

If you only want to connect one shop's own system, such as its ERP or CRM, you do not need a connector. See Your own systems over MCP.

The one rule

A new provider is a new folder, and nothing else. No change to the API, the agent, the merchant app or the database. The merchant app draws the connector's card and its Connect form from the folder's manifest; the registry picks it up; webhooks are routed to it by its manifest.

If adding a provider seems to need a change outside connectors/, that is a bug in the framework. Fix the framework, not the connector. A test in the API writes a made-up connector into a temporary folder and checks the registry picks it up with no other change.

Four kinds

Every connector is one of four kinds. The rest of the system asks for "this shop's payments connector", never for a provider by name, and calls only the methods of that kind's contract:

  • catalog: list_products, get_product, get_stock, watch_changes, and optionally write_back.
  • payments: create_payment_link, get_payment, refund, and optionally settlement_status.
  • cpaas (WhatsApp and SMS): send_text, send_template, send_media, send_interactive, get_delivery_status. A provider that declares the whatsapp capability also gets the 24-hour window and template rules enforced for it.
  • email: send, send_template, get_delivery_status.

Every connector also has check_health and, if it receives webhooks, verify_webhook and handle_webhook.

The contracts live in connectors/_base/src/resandhi_connectors/kinds/, and every provider extends BaseConnector in connectors/_base/src/resandhi_connectors/base.py.

1. Scaffold the folder

make new-connector KIND=payments PROVIDER=cashfree

This creates connectors/payments-cashfree/ with:

manifest.yaml        identity, kind, auth fields, settings, capabilities, webhooks
provider.py          the provider class, with every contract method stubbed
server.py            exposes the connector as an MCP server
README.md            the setup steps a merchant sees in the app
tests/
  conftest.py
  test_contract.py   wired to the shared contract harness

The list of methods in provider.py is read from the kind's contract itself, so it is always current. Every stub raises NotImplementedError on purpose: a stub that returned believable fake data would pass its own tests and get merged.

2. Fill in the manifest

manifest.yaml is the connector's whole description of itself. A trimmed example:

id: payments-cashfree # must equal the folder name
kind: payments # catalog | payments | cpaas | email
name: Cashfree
version: 0.1.0

summary: >-
  One or two sentences for the connector card. Say what it does for the
  merchant, not how it works.

auth:
  type: api_key # api_key | oauth2 | none
  fields:
    - key: app_id
      label: App ID
      secret: false
      help_text: Where to find it, in the words of the vendor's own dashboard.
    - key: secret_key
      label: Secret key
      secret: true

config_schema: # JSON Schema for settings that are not secret
  type: object
  additionalProperties: false
  properties: {}

capabilities: [payment_link, refund, webhook_verify]

webhooks:
  - path: /webhooks/payments/cashfree
    verify: hmac_sha256

icon: icon.svg
health: provider.health
regions: [IN]

Things worth knowing:

  • secret defaults to true. Secret fields go to the secrets store and are never shown again. Forgetting the flag fails safe.
  • Only declare capabilities you really support. Callers check a capability before calling an optional method, and an undeclared one is refused cleanly, which lets the system fall back to another install. Claiming a capability you cannot honour is worse than leaving it out.
  • auth.type: oauth2 needs an oauth block with authorize_url, token_url and scopes. auth.type: none must declare no fields.
  • The icon ships in the folder. Put icon.svg beside the manifest; the merchant app shows it on the card. No frontend change is needed.

3. Implement the provider

A connector is handed three things: its install (with the merchant's settings), a view of its secrets, and an HTTP client with timeouts already set. It never gets a database session or anything about other shops.

A few rules the base class and harness hold you to:

  • Use self._request(...) for vendor calls. It turns timeouts and connection errors into ProviderUnavailable, which the gateway treats as retryable and can fail over to a backup install.
  • check_health makes one cheap, authenticated call and must never create, charge or send anything. It runs every time the Connections page loads, and is cut off after 2 seconds.
  • Every write takes an idempotency key. A retried send must never create a second payment link for the same order.
  • No card data, ever. Payment providers return a hosted link or page. There is no method that accepts a card number, and there must never be one.
  • handle_webhook returns events and does nothing else. No sends, no writes. The caller stores the events in one transaction, so a retried webhook cannot half-apply.
  • Never put a vendor's raw error text in front of a merchant. It often contains the request URL, and request URLs often contain keys.

4. Verify webhooks properly

If the manifest declares a webhook, verify_webhook must check the vendor's signature. The default implementation refuses everything, so a forgotten method fails closed.

Incoming webhooks arrive on one route, /webhooks/{kind}/{provider}. The install is found, the signature is checked with your verify_webhook, the raw payload is stored, and your handle_webhook runs later in a worker. Nothing is processed inline.

5. Test against the shared harness

tests/test_contract.py builds your connector against recorded vendor responses and hands it to the shared harness. The harness checks:

  1. The manifest parses, its id matches its folder, and its capabilities are spelled correctly.
  2. The class extends its kind's contract and implements every required method.
  3. A declared webhook has a real verify_webhook, and it rejects a tampered body. A verifier that always returns true passes every other test; this one catches it.
  4. health() returns within its time limit and never raises.
  5. Optional methods you did not declare are refused with CapabilityNotSupported, not a crash.

Replace the canned responses in the scaffold with real ones recorded from the vendor's API. The harness is only as good as what it replays. Then run:

make test-connectors

If your vendor does not sign the raw body (some send a shared token, some sign selected fields), you can supply your own tampered and unsigned requests to the harness. That weakens a guarantee, so say so in the connector's README.

6. Write the README for a shop owner

README.md is shown as-is in the merchant app, under the Connect button. Write it for a shopkeeper, not an engineer:

  1. Numbered steps that match what they see on the vendor's own screens.
  2. Say where to click, not what an API key is.
  3. If something can go wrong, say what it looks like and what to do.
  4. One honest paragraph on what you read, what you write, and what you never touch.

7. Dependencies

Connector folders are not Python packages, so a connector cannot declare its own dependencies. If yours needs a new library, add it to the connectors dependency group in the root pyproject.toml, and say why in the pull request.

Before you open a pull request

  • The folder is the only thing that changed.
  • make test-connectors passes, with recorded vendor responses.
  • The README walks a shop owner through setup.
  • Capabilities in the manifest match what the provider really does.