Developers · Connect v1

Gateway & customer API

How customers call your module's API through api.cosuite.io with their CoSuite key, and how your app verifies and serves those requests (contract 1.8).

Customers call your module's API at https://api.cosuite.io/v1/apps/{app}/{path} with their own CoSuite API key. CoSuite checks the key, the subscription and the scope, then forwards the request to your app with a Connect signature and the calling business in X-CoSuite-Tenant. Your app never sees the customer's key, and the customer never sees yours.

Customer software ──Bearer cs_api_…──▶ api.cosuite.io/v1/apps/dent/patients
                                            │  key · subscription · route · scope · rate limit
                                            ▼
                      {server}/patients  ◀── Connect signature + X-CoSuite-Tenant

For your app: serving gateway requests

The gateway only forwards operations that exist in your module's OpenAPI 3.1 document, and only when the customer's key holds a scope the operation requires.

Address. Requests go to servers[0].url of your OpenAPI document when it is on the same origin as your app's base URL; otherwise to the base URL. Path and query are appended unchanged: GET /v1/apps/dent/patients?page=2 → GET {server}/patients?page=2.

Headers you receive (in addition to the Connect signature headers):

Header Meaning
X-CoSuite-Gateway: 1 The request comes from the gateway, not from a Connect call.
X-CoSuite-Tenant The business's tenantId (the same id as in POST /tenants). Return only this business's data.
X-CoSuite-Scopes The key's scopes valid for this request, space separated.
X-CoSuite-Api-Key-Id Id of the customer's CoSuite key (not the key itself), for your logs and limits.
Content-Type, Accept, Accept-Language, If-Match, If-None-Match Passed through from the customer.

The customer's Authorization header is never forwarded.

Your obligations:

  • Verify the signature and reject replayed request ids. verifyGatewayRequest does both.
  • For an unknown or unlinked tenantId, answer 404 with tenant_not_found and return no data.
  • Answer within 30 seconds, at most 10 MB, and never with a redirect (3xx is treated as an error).
  • The gateway already checked the operation's scope; you may narrow further using X-CoSuite-Scopes.
import { verifyGatewayRequest, MemoryReplayStore } from "@cosuite/connect";

const replay = new MemoryReplayStore(); // use Redis or your database with several processes

export async function GET(req: Request) {
  const url = new URL(req.url);
  const body = await req.text();
  const v = await verifyGatewayRequest({
    method: req.method,
    pathAndQuery: url.pathname + url.search,
    body,
    headers: req.headers,
    secretsFor: (keyId) => (keyId === process.env.COSUITE_KEY_ID ? [process.env.COSUITE_SECRET!] : undefined),
    replay,
  });
  if (!v.ok) return Response.json({ ok: false, error: { code: "unauthorized", message: v.reason, retryable: false } }, { status: 401 });

  const account = await findAccountByCoSuiteTenant(v.context.tenantId);
  if (!account) return Response.json({ ok: false, error: { code: "tenant_not_found", message: "tenant not found", retryable: false } }, { status: 404 });

  return Response.json({ items: await listPatients(account.id) });
}

In other languages, verify the signature exactly as for Connect calls and read the headers above.

Conformance. For every module with an api, the conformance test calls up to three parameterless GET operations:

  1. With the test business: an answer is expected.
  2. With an unknown business: 404 tenant_not_found is expected.
  3. Without a signature: 401 is expected.

For customers: calling the API

  1. In the CoSuite panel, open Management › API keys and create a key. Choose the app and only the scopes you need; set an expiry if you can. The key is shown once.
  2. Send it as a bearer token:
curl -H "Authorization: Bearer $COSUITE_API_KEY" \
  "https://api.cosuite.io/v1/apps/dent/patients?page=2"

The key works only for the app it was created for, only with the scopes you chose, and only while the module is in your subscription. When you drop the module its scopes stop working; revoked or expired keys fail immediately. Every call is recorded in the API audit log (key, endpoint, scope, status, duration — never bodies) and kept for 12 months.

Limits:

  • Request body: at most 2 MB.
  • 600 requests per minute per business, 300 per minute per key.
  • An app that fails 5 times in a row is paused for 60 seconds.

Webhooks

Your app declares the events a module emits in the manifest (modules[].webhooks[], see module definition) and sends each occurrence to CoSuite like any other event: POST /connect/v1/events with tenantId, a unique id, the declared type and data. CoSuite forwards it to the businesses that subscribed and does not keep the body after delivery.

Customers subscribe in Management › API keys › Webhooks: they pick the app, the events and an https address, and receive a signing secret once. Each delivery is a POST with the JSON body { "id", "type", "app", "occurredAt", "data" } and these headers:

Header Meaning
X-CoSuite-Event Event type, e.g. treatment.completed
X-CoSuite-Delivery Delivery id (the same on every retry)
X-CoSuite-Timestamp Unix seconds
X-CoSuite-Signature v1= + hex HMAC-SHA256 of {timestamp}.{raw body} with the secret

Verify the signature, accept timestamps within 5 minutes, and answer 2xx within 10 seconds. Failed deliveries are retried for up to 24 hours (after 1, 5, 15, 60, 180, 360 and 720 minutes). Use the event id to ignore duplicates.

Errors

The app's own responses (status and body) are returned unchanged. Errors produced by the gateway use the Connect envelope { "ok": false, "error": { "code", "message", "retryable" } }:

Status Code When
401 unauthorized Missing, invalid, revoked or expired key
403 forbidden The key belongs to another app
403 not_subscribed None of the key's scopes are in the current subscription
403 insufficient_scope The operation needs a scope the key doesn't have
404 app_not_found · route_not_found Unknown app, or the path isn't in the app's API
405 method_not_allowed The path exists, the method doesn't
409 not_linked The business hasn't been linked to the app yet (open it once from CoSuite)
413 payload_too_large Body over 2 MB
429 rate_limited Rate limit exceeded; see Retry-After
502 bad_gateway The app couldn't be reached, redirected, or answered over 10 MB
503 app_unavailable The app is paused after repeated failures; see Retry-After
504 upstream_timeout The app didn't answer within 30 seconds

Every gateway response carries X-CoSuite-Request-Id; quote it when you contact support.