Team-Connect Engineering Guide · Updated 3 May 2026

API Documentation

A practical engineer's guide to REST API integration — how to authenticate, handle rate limits gracefully, paginate without losing data, retry safely with idempotency keys, parse error responses correctly, and avoid the silent integration failures that catch every team eventually.

REST + JSON · Auth, rate limits, pagination, errors · curl + Node + Python code
Jump to a section

01What is a REST API?

A REST API (Representational State Transfer) is a way for two systems to communicate over HTTP using a small set of conventions: URLs identify resources, HTTP verbs (GET, POST, PUT, PATCH, DELETE) describe actions, status codes communicate outcomes, and JSON typically carries the data. Most modern web APIs are REST or REST-adjacent. The benefit is universality — any language with an HTTP client can call a REST API, and any developer who knows HTTP can read one with no extra learning curve.

The core conventions

A well-designed REST API maps cleanly to the resources it exposes. For an object type called calls, the conventional URL space is:

VerbURLPurposeTypical response
GET/v1/callsList calls (paginated)200 with JSON list
GET/v1/calls/{id}Read one call200 with JSON object, or 404
POST/v1/callsCreate a new call201 with the created resource
PATCH/v1/calls/{id}Update specific fields200 with the updated resource
PUT/v1/calls/{id}Replace the entire resource200 with the new resource
DELETE/v1/calls/{id}Delete the resource204 with no body, or 200 with the deleted record

Why this matters in practice

Once you internalise the verb-on-URL pattern, every well-designed REST API follows the same shape. You read documentation faster, your code is easier to skim, and switching between providers is mostly mechanical: change the base URL, change the auth header, the rest is similar enough. The places APIs differ matter (auth, rate limits, pagination, error format, versioning) and we cover each below.

REST is not a strict spec

"REST" was coined in Roy Fielding's 2000 PhD thesis and described an architectural style, not a wire format. Most APIs that call themselves REST violate parts of his definition (HATEOAS in particular is rare), and the term has settled into meaning "JSON over HTTP with conventional verbs and URLs". For practical purposes that's the working definition you need; the academic purity arguments rarely matter to integration code.

What about GraphQL, gRPC and others?

  • GraphQL is a single-endpoint query language; clients ask for exactly the fields they want. Strong for client-driven UIs (mobile apps, complex dashboards). Weaker for fixed integration patterns where REST simplicity wins.
  • gRPC is a binary RPC protocol over HTTP/2 with auto-generated client code from .proto files. Very fast, very strongly typed, used heavily inside service-to-service backends. Less common as a public API surface because it is harder to call from random tools.
  • JSON-RPC, SOAP, XML-RPC exist; you encounter them mostly in legacy or specific-vendor contexts.

For most public-facing APIs in 2026, REST + JSON is the pragmatic default and what this guide covers. The Team-Connect API is REST + JSON; the patterns here apply directly.

The right mental model: REST is a vocabulary. The URLs are nouns, the verbs are actions, the status codes are outcomes, the body is data. If an API forces you to send a verb in the URL or a noun in the verb (POST /getUserByEmail with a body containing the email), it is not really REST — just RPC dressed in REST clothes. Real REST APIs read like sentences.

02Authentication

Every API call has to prove who you are. The patterns have converged in 2026 around a small set of standard approaches; pick one that matches the trust level of your integration and apply it consistently.

The four authentication patterns you will encounter

PatternWhere the credential goesWhen to use it
Bearer tokenAuthorization: Bearer <token> headerDefault for most modern APIs. Simple, widely tooled.
OAuth 2.0Bearer token obtained via auth flow, refreshed periodicallyWhen users grant permission to a third-party app on their behalf.
HMAC signed requestsSignature header computed over request body + timestampBanking, healthcare, very high-security server-to-server.
mTLSClient certificate at the TLS layerHighly regulated industries; defence beyond bearer tokens.

Bearer token (the default in 2026)

A bearer token is a long opaque string that proves identity. You include it in the Authorization HTTP header on every request:

Bearer token authentication

GET /v1/calls HTTP/1.1
Host: api.team-connect.co.uk
Authorization: Bearer tc_live_8f3a7b2c4d5e6f1a9b8c7d6e5f4a3b2c
Content-Type: application/json

"Bearer" means "anyone presenting this token gets authenticated as the owner". The token itself is the secret — protect it accordingly. Common token conventions you will see:

  • Prefixes that signal scope: tc_live_, tc_test_, sk_live_, pk_live_. Lets you spot at a glance whether you have a production or test key, secret or public key.
  • Random length 32-64 chars: long enough that brute-forcing is hopeless.
  • Never embedded in URLs: URLs end up in server logs, browser history, proxy caches, error trackers. Always send tokens in headers.

Storing API keys safely

  1. Environment variables for application configuration. Never hard-code keys in source.
  2. Secret manager for production: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, 1Password Secrets Automation. Keys never live on disk in plaintext.
  3. Different keys per environment: separate dev, staging and production tokens. Compromising staging should not compromise production.
  4. Rotation policy: rotate keys quarterly minimum, immediately on suspected compromise. Most providers support multiple active keys so you can rotate without downtime.
  5. Scope keys narrowly: if the API supports per-resource permissions, give each integration the minimum scope it needs (read-only for analytics, write-only for ingest, etc.). The principle of least privilege.

OAuth 2.0 in one paragraph

If your application acts on behalf of users rather than as itself, you need OAuth. The flow: user clicks "connect", you redirect to the provider's authorization page, user grants permission, provider redirects back with an authorization code, you exchange the code for an access token (short-lived, often 1 hour) and a refresh token (long-lived). On API calls, you send the access token as a Bearer; when it expires, you use the refresh token to get a new one without re-prompting the user. This is what "Sign in with Google" buttons are doing under the hood.

What never to do with API keys

  • Never put them in URL query strings. They will leak through Referer headers, web server logs, browser history, error tracking dashboards, proxy caches.
  • Never embed them in client-side code. Anyone can view the source of a web page or decompile a mobile app. Public client code can have public-readable keys at most; for write actions, route through your server.
  • Never commit them to source control. Even private repos are not safe — people get added, repos get cloned, archives get exposed. Use .env files, secret managers, or build-time injection.
  • Never reuse a personal key for production traffic. Personal keys are tied to your individual account; if you leave the company, the keys typically get revoked, and your production goes down. Use service accounts.
The most common API key leak: developer accidentally commits a .env file or a config script to GitHub. Within minutes, automated scanners find it and start using the key. By the time you notice, you have a six-figure API bill and your data has been exfiltrated. Mitigations: GitHub secret scanning (free, automatic), pre-commit hooks that block committing files matching known credential patterns (truffleHog, gitleaks), and treating any leaked credential as compromised — rotate immediately, do not just remove the commit.

03Your First Request

The fastest way to learn an API is to make a real call. Here is the minimum-viable Team-Connect API request in three languages. Substitute your API key (from the dashboard) and you have a working integration.

curl (test from any terminal)

List recent calls with curl

curl https://api.team-connect.co.uk/v1/calls \
  -H "Authorization: Bearer tc_live_YOUR_KEY_HERE" \
  -H "Accept: application/json"

Add -i to see headers, -v to see the full conversation. For POST requests:

Send an SMS via curl (POST with JSON body)

curl -X POST https://api.team-connect.co.uk/v1/messages \
  -H "Authorization: Bearer tc_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+441625555123",
    "from": "+441625555456",
    "body": "Hello from Team-Connect"
  }'

Node.js (using the standard fetch API)

Same request in Node.js with fetch

const API_KEY = process.env.TEAMCONNECT_API_KEY;

async function listCalls() {
  const response = await fetch('https://api.team-connect.co.uk/v1/calls', {
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`API error ${response.status}: ${await response.text()}`);
  }

  return response.json();
}

const data = await listCalls();
console.log(`Got ${data.calls.length} calls`);

Python (using requests)

Same request in Python

import os
import requests

API_KEY = os.environ["TEAMCONNECT_API_KEY"]
BASE_URL = "https://api.team-connect.co.uk/v1"

response = requests.get(
    f"{BASE_URL}/calls",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json",
    },
    timeout=10,
)
response.raise_for_status()  # raises on 4xx/5xx

data = response.json()
print(f"Got {len(data['calls'])} calls")

Wrap your client once, call it many times

For anything beyond a one-off script, wrap the API in a thin client class so you set base URL, auth header, default timeouts and error handling once. This is what every SDK does internally; for small integrations you can build your own.

Minimal Python API client wrapper

import os
import requests
from typing import Any

class TeamConnectClient:
    def __init__(self, api_key: str | None = None, base_url: str = "https://api.team-connect.co.uk/v1"):
        self.api_key = api_key or os.environ["TEAMCONNECT_API_KEY"]
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Accept": "application/json",
        })

    def _request(self, method: str, path: str, **kwargs) -> Any:
        kwargs.setdefault("timeout", 30)
        response = self.session.request(method, f"{self.base_url}{path}", **kwargs)
        response.raise_for_status()
        return response.json() if response.content else None

    def list_calls(self, **params): return self._request("GET", "/calls", params=params)
    def get_call(self, call_id: str): return self._request("GET", f"/calls/{call_id}")
    def send_sms(self, **body): return self._request("POST", "/messages", json=body)

# Usage
client = TeamConnectClient()
recent_calls = client.list_calls(limit=20)
client.send_sms(to="+441625555123", from_="+441625555456", body="Hi!")

This client wrapper is about 30 lines and gives you connection pooling (via the Session), default headers, default timeouts and clean call-site syntax. Build it once, use it everywhere.

Always set a request timeout. The default in many HTTP libraries is "wait forever". A hung connection means your worker thread is stuck, your queue backs up, and eventually your service is down for one slow downstream call. Set timeouts of 10-30 seconds on every API call, with shorter timeouts for synchronous user-facing flows (5-10 seconds) and longer for batch jobs (60+).

04Rate Limiting

Rate limiting is how an API enforces fair usage by capping how many requests a client can make in a time window. When you exceed the cap, the API returns 429 Too Many Requests. Your job as a client is to (a) avoid hitting the cap when you can predict load, and (b) handle 429 gracefully when you cannot.

The standard rate limit headers

Most modern APIs expose three response headers on every request so you can self-throttle:

HeaderMeaningExample
X-RateLimit-LimitMax requests per window1000
X-RateLimit-RemainingRequests left in current window847
X-RateLimit-ResetWhen the window resets (Unix seconds or seconds-until-reset)1714723200
Retry-AfterOn 429: how long to wait before retrying (seconds)30

Naming varies between providers (X-RateLimit-*, RateLimit-* per the IETF draft, X-Rate-Limit-*). Read your provider's docs to know which.

Handle 429 with exponential backoff and jitter

The minimum-viable 429 handler retries after the period in Retry-After. The robust handler uses exponential backoff with jitter so a fleet of clients hitting a limit at the same time does not all retry simultaneously.

Node.js: 429 handling with exponential backoff and jitter

async function callWithRetry(url, options = {}, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    if (attempt === maxRetries) {
      throw new Error('Rate limit retries exhausted');
    }

    // Respect Retry-After if provided
    const retryAfter = parseInt(response.headers.get('Retry-After') || '0', 10);
    const baseWait = retryAfter * 1000 || (1000 * Math.pow(2, attempt));

    // Add jitter: 0-25% extra to spread out concurrent retries
    const jitter = Math.random() * baseWait * 0.25;
    const waitMs = baseWait + jitter;

    console.log(`429 received, waiting ${Math.round(waitMs)}ms (attempt ${attempt + 1})`);
    await new Promise(r => setTimeout(r, waitMs));
  }
}

Self-throttle before you hit the limit

For known high-volume work (bulk imports, scheduled jobs, batch reports), check X-RateLimit-Remaining on responses and pace yourself accordingly. A simple token-bucket algorithm in your client lets you spend requests at a sustainable rate rather than reactively backing off.

Python: simple token bucket for client-side rate limiting

import time
import threading

class TokenBucket:
    def __init__(self, rate_per_sec: float, capacity: int):
        self.rate = rate_per_sec
        self.capacity = capacity
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self) -> None:
        with self.lock:
            now = time.monotonic()
            elapsed = now - self.last_refill
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_refill = now

            if self.tokens < 1:
                wait = (1 - self.tokens) / self.rate
                time.sleep(wait)
                self.tokens = 0
            else:
                self.tokens -= 1

# Limit yourself to 10 requests per second, max burst of 20
bucket = TokenBucket(rate_per_sec=10, capacity=20)

for item in big_list:
    bucket.acquire()
    response = client.do_thing(item)

Why "fixed window" vs "sliding window" matters

Rate limit implementations come in two flavours:

  • Fixed window: the limit resets at fixed clock boundaries (e.g. every minute). Simple to implement but allows traffic spikes at boundaries (you can hit your full minute's quota in the last second of one minute and the first second of the next, doubling your effective rate briefly).
  • Sliding window: the limit applies to any rolling time window. More accurate enforcement; more complex to implement. Most modern providers use sliding windows.

You usually do not need to know which your provider uses. But if you see "I have plenty of remaining quota and got 429 anyway", the answer is often "the burst behaviour of the limiter is stricter than the headers suggest". Pace requests evenly rather than burst-firing them.

Rate limits at Team-Connect

Default rate limits are per-API-key:

  • Standard plan: 100 requests per minute, burst of 200.
  • Pro plan: 1,000 requests per minute, burst of 2,000.
  • Enterprise: custom limits, contact us.

Bulk operations have separate higher limits per endpoint. Webhook delivery does not consume your API quota.

The single biggest rate limit anti-pattern: retrying immediately on 429 with no delay. This makes the problem worse — the API is already telling you to slow down, and slamming it again means you stay rate-limited longer. Always respect Retry-After; if there is no Retry-After, default to exponential backoff starting at 1-2 seconds. The combined "exponential + jitter + Retry-After when present" pattern is robust against essentially all rate-limit scenarios.

05Pagination

Almost any list endpoint will return a paginated subset rather than every record. The two dominant approaches — offset and cursor — have very different reliability characteristics under real-world conditions, and choosing wrong is a recurring source of "we are losing records during nightly imports" type bugs.

Offset pagination (the older, simpler pattern)

Offset request

GET /v1/calls?limit=50&offset=100

"Give me 50 records starting from position 100." Easy to understand, easy to expose as page numbers in a UI. Two big problems:

  1. Inconsistent under writes. If a record is added or deleted between your "page 1" and "page 2" fetches, page 2 will start at the wrong offset — you will skip records or see duplicates. For a list ordered by creation time with new records arriving constantly (calls, events, transactions), this happens routinely.
  2. Performance degrades with offset depth. The database has to count and skip every row before the offset. OFFSET 100 is fine; OFFSET 1,000,000 is not. Some databases will time out before returning a deep page.

Cursor pagination (the modern default)

Cursor request

GET /v1/calls?limit=50&cursor=eyJpZCI6Ijg0MmMzNzU2In0

"Give me 50 records after the position represented by this opaque token." The cursor is typically a base64-encoded JSON of the last item's sort key (e.g. {"id": "842c3756"}). The API returns records and a new cursor for the next page; you keep going until the API returns no more cursor or an empty list.

  • Stable under writes. The cursor is anchored to a real record's sort key, not to a position. New records inserted earlier in the list do not shift your iteration.
  • Consistently fast. Each page is "find records with sort key > cursor" — an indexed lookup, regardless of how deep you are.
  • No total count. You typically cannot ask "how many pages are there in total" because the cursor is the only positional information you have. This is a deliberate trade-off — counting the entire collection is the part that scaled badly.

Side-by-side

AspectOffset paginationCursor pagination
Stability under writesRecords can be skipped or duplicatedStable; new records do not shift iteration
Performance at depthSlow at deep offsets (linear scan)Fast at any depth (indexed seek)
UI page-number displayEasy ("page 7 of 42")Hard (no fixed positions)
Total countUsually includedUsually omitted
Random accessYes (jump to any page)No (sequential only)
Best forStatic-ish data, UIs needing page numbersLive-changing data, automated iteration

The full-iteration loop

For batch jobs that need to pull everything, the standard pattern is a while-loop that keeps following cursors until exhausted:

Python: full iteration of a cursor-paginated endpoint

def iter_all_calls(client):
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor

        response = client.list_calls(**params)
        for call in response["data"]:
            yield call

        cursor = response.get("next_cursor")
        if not cursor:
            break

# Use it
for call in iter_all_calls(client):
    process(call)

Use a generator (yield in Python, async iterators in Node) so the consumer can process each item as it arrives without loading the entire dataset into memory.

Don't fight the order

Pagination is always tied to an explicit sort order — the cursor encodes "where you are" in some specific ordering. If you need a different order than the default, the API may or may not support it; do not assume sorts are interchangeable. Reading "newest calls first" and then asking "all calls in chronological order" requires either re-iterating with a different sort param (if supported) or sorting in your code after the fact.

If your iteration touches state during the loop: be especially careful to use cursor pagination, not offset. A common bug pattern is "iterate over all records, mark each as processed; the count comes out wrong because some records get skipped while others are seen twice". This is offset pagination interacting badly with concurrent writes from your own code — cursor pagination eliminates the class entirely because the cursor is bound to a record, not a position.

06Error Handling

Every API call can fail. Robust integration code treats errors as a first-class concern: the right HTTP status code maps to the right action, structured error bodies surface the right diagnostic detail, and retry logic distinguishes transient from permanent failures.

HTTP status codes you will actually see

CodeMeaningWhat you should do
200 OKSuccessful readUse the response body
201 CreatedSuccessful write that created a resourceUse the response (often contains the created resource)
204 No ContentSuccessful action with no bodyTreat as success; do not parse body
400 Bad RequestMalformed requestFix your request; do not retry
401 UnauthorizedMissing or invalid authCheck your API key; re-authenticate; do not retry blindly
403 ForbiddenAuthenticated but not allowedWrong permissions; contact your admin or check scope
404 Not FoundResource does not existCheck the ID; do not retry
409 ConflictConflicts with current state (e.g. duplicate, version mismatch)Read the body for guidance; might need to re-read state and retry
422 Unprocessable EntityValidation failure on otherwise well-formed requestRead the error body for which field is invalid; do not retry without changing it
429 Too Many RequestsRate limit hitWait per Retry-After, then retry (see section 04)
500 Internal Server ErrorServer bug or unexpected conditionRetry with backoff; if persistent, report to provider
502 Bad GatewayUpstream service failureRetry with backoff
503 Service UnavailableService overloaded or in maintenanceRespect Retry-After if present; retry with backoff
504 Gateway TimeoutUpstream timed outRetry with backoff; check whether request was actually processed (use idempotency key)

The standard error envelope

Most modern APIs return error details in a structured JSON body, not just the status code:

A typical 422 response

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": {
    "type": "validation_error",
    "code": "invalid_phone_number",
    "message": "The 'to' field must be a valid E.164 phone number.",
    "param": "to",
    "request_id": "req_8e3a4b2c5f1d4789"
  }
}

Standard fields you will encounter (names vary; the concepts are universal):

  • type or category — high-level class (validation_error, authentication_error, rate_limit_error, server_error).
  • code — machine-readable identifier (invalid_phone_number, insufficient_balance). Use this in your code, not the message.
  • message — human-readable description. Surface this to developers, not necessarily to end users.
  • param or field — which input was rejected (for validation errors).
  • request_id — unique ID for this specific call. Capture in your logs; include when contacting support.

The retry decision tree

Whether to retry depends on the failure class:

  • Network errors (no response, connection refused, DNS failure) → retry with backoff. The server may not have received the request.
  • Timeout (no response within your client timeout) → retry with backoff, but the server may have processed the request — use idempotency keys (section 08).
  • 4xx (except 408 and 429) → do not retry. Your request is wrong; retrying does not change that.
  • 408 Request Timeout → retry once or twice; treat like a network timeout.
  • 429 Too Many Requests → retry per Retry-After; see section 04.
  • 5xx → retry with exponential backoff. Cap the retries (3-5 attempts) so a persistently broken downstream does not pin your worker.

The "log everything" principle

Every API call your service makes should produce structured log lines suitable for grep and aggregation. Minimum fields:

  • Method, URL, response status
  • Latency in milliseconds
  • Provider's request_id if returned (essential for support)
  • Internal trace ID linking this call to the user request that caused it
  • Error type and code on failures (parsed from response body)

When something goes wrong six months from now, this log is what you have. The team that has it solves problems in minutes; the team that does not stares at "Internal Server Error" with no context.

Never silently swallow an error. Common bug pattern: try { await api.call() } catch (e) { /* ignore */ }. The next time someone debugs why a feature is silently broken, they will spend hours tracing through code only to find the swallowed error. If you genuinely do not care about a failure, log it at warn level so it shows up; if you do care, propagate it. The middle ground — catching and discarding — is always wrong.

07Versioning and Deprecation

APIs change. Field shapes evolve, response formats refine, new fields appear, occasional breaking changes happen. The contract between API provider and consumer is: breaking changes only happen across version boundaries, additive changes are safe within a version, and deprecation comes with a published timeline.

The two versioning approaches

ApproachHow it worksProsCons
URL-basedVersion segment in the path: /v1/calls, /v2/callsVisible, easy to route, clear in logs and codeCoarse-grained; v1 to v2 is a big bang
Header-basedVersion in a request header: Stripe-Version: 2026-04-01Fine-grained per-request control; can pin exactlyLess visible; harder to glance at a URL and know the version

URL versioning is more common. Header versioning (popularised by Stripe with date-based versions like 2026-04-01) gives more flexibility and is gaining ground. Some APIs use both: major version in URL, fine-grained version in header.

What "additive" vs "breaking" means

The distinction is what an existing client experiences without changes:

  • Additive (safe within a version) — new optional fields in responses, new optional parameters on requests, new endpoints, new event types, new error codes. Existing clients ignore the additions; nothing breaks.
  • Breaking (requires a new version) — removing a field, renaming a field, changing a field's type, making a previously optional parameter required, changing a default behaviour, removing an endpoint, changing the meaning of an existing value.

The deprecation lifecycle

Responsibly run APIs follow a multi-step deprecation process:

  1. Announce — new behaviour available; old behaviour still default and supported. Documentation explains the migration path.
  2. Default switch — new behaviour becomes default for new accounts; existing accounts unchanged.
  3. Sunset notification — date set for removal of old behaviour; emails sent to API key owners; deprecation headers added to responses (Sunset: Mon, 01 Jan 2027 00:00:00 GMT, Deprecation: true).
  4. Removal — sunset date arrives; old behaviour stops working. Calls return 410 Gone or 4xx with a clear message.

The minimum acceptable timeline is 6 months between sunset notification and removal; many APIs run 12+ months for major changes.

How to survive API upgrades

As an API consumer, three habits keep you safe:

  • Pin your version explicitly. Specify the exact version in your client's default headers; do not let it drift. Stripe-Version: 2026-04-01 means you get exactly what was current on that date, even after Stripe ships changes.
  • Subscribe to the changelog. Every reputable API provider publishes a changelog or release notes. Read them.
  • Watch for deprecation headers. Log responses that include Sunset or Deprecation headers; alert when they appear so you have lead time before something breaks.

Detecting deprecation headers in your client

// Add a response interceptor that logs deprecation warnings
client.interceptors.response.use(response => {
  const sunset = response.headers.get('sunset');
  const deprecation = response.headers.get('deprecation');

  if (sunset || deprecation) {
    logger.warn('API deprecation warning', {
      url: response.url,
      sunset, deprecation,
      api_version: response.headers.get('api-version')
    });
  }

  return response;
});

What to do when an API breaks anyway

Sometimes providers break their API without proper deprecation, ship regressions, or change behaviour silently. When this happens:

  • Reproduce against staging with the smallest possible test case.
  • Capture full request and response including all headers and the request_id.
  • File a bug with the provider, citing the request_id.
  • Implement a workaround in your code with a comment pointing to the bug; remove it once the provider fixes the underlying issue.
The version pin antipattern: hardcoding a version in dozens of code paths instead of one. When the version changes, you have to find and update every site, miss one, and ship a partial migration. Always set the version once in your HTTP client's default headers; treat the version as configuration, not as code.

08Idempotency Keys for Safe Writes

Every write to an API is potentially in trouble: the request can succeed but the response can get lost. Your client times out and retries; the server processes the request twice. The user is double-charged, the message is sent twice, the booking is duplicated. Idempotency keys solve this — they let the server detect retries and return the same result without re-doing the work.

How idempotency keys work

Before sending a write request, you generate a unique random ID (typically a UUID v4) and include it in a header:

Sending a request with an idempotency key

POST /v1/messages HTTP/1.1
Authorization: Bearer tc_live_...
Content-Type: application/json
Idempotency-Key: 7c8a4e2f-9b1d-4e58-a3c7-6d8e9f0a1b2c

{
  "to": "+441625555123",
  "from": "+441625555456",
  "body": "Hi from Team-Connect"
}

The server processes the request and stores the result indexed by the idempotency key. If you retry with the same key, the server returns the stored result without re-sending the SMS. The key is your responsibility to generate; the server only knows whether it has seen it before.

The retry-safe write pattern

Python: idempotent write with retries on transient failures

import uuid
import time
import requests

def send_sms(to: str, body: str, max_retries: int = 3) -> dict:
    idempotency_key = str(uuid.uuid4())  # generate ONCE outside the retry loop

    for attempt in range(max_retries):
        try:
            response = requests.post(
                "https://api.team-connect.co.uk/v1/messages",
                headers={
                    "Authorization": f"Bearer {API_KEY}",
                    "Idempotency-Key": idempotency_key,
                    "Content-Type": "application/json"
                },
                json={"to": to, "body": body},
                timeout=30
            )

            if response.status_code < 500:
                response.raise_for_status()
                return response.json()

            # 5xx: retry with backoff
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt + random.random())
                continue
            response.raise_for_status()

        except (requests.Timeout, requests.ConnectionError):
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt + random.random())
                continue
            raise

    raise RuntimeError("Retries exhausted")

The crucial detail: generate the idempotency key once, outside the retry loop. If you generate a fresh key per attempt, every retry looks like a new request to the server and you lose the safety. Same key, different attempts.

Where to store the idempotency key on your side

For a fire-and-forget retry inside one process, generating a UUID at request time and reusing it across retries is enough. For more robust patterns where the request might be retried by a queue worker that crashed and restarted:

  • Persist the idempotency key alongside the work item in your queue.
  • When the worker picks up the item, use the persisted key for every retry.
  • This makes the entire write operation safe across worker crashes, deploys, restarts.

How long the server remembers idempotency keys

Most providers cache idempotency results for 24-72 hours. After that, the key is forgotten and a new request with the same key is treated as fresh. Plan retries to fit inside that window. If you need longer (e.g. a quarterly batch job that retries from scratch), regenerate keys per batch.

What idempotency keys do NOT do

  • They don't make a server-side bug idempotent. If the API processed your request, returned a response, but server crashed before storing the idempotency record, your retry will double-execute. (Most providers handle this correctly; rare to hit in practice.)
  • They don't replace your own idempotency. If your business logic creates side effects beyond the API call (writing to your own DB, triggering downstream events), those need their own deduplication. Idempotency keys only protect the API write itself.
  • They don't survive across providers. An idempotency key is a per-API-call construct; it doesn't carry meaning to a different API.

Which APIs require idempotency keys

  • Always required: Stripe POST /v1/charges, Square POST /v2/payments, most payment APIs.
  • Recommended: Twilio, Adyen, GitHub create operations, most modern APIs for any state-changing call.
  • Available but optional: Team-Connect supports the Idempotency-Key header on all POST/PUT/PATCH endpoints. Use it on writes that have user-visible side effects (sending SMS, creating calls, charging accounts).
Make idempotency keys part of your client wrapper. Rather than passing them everywhere by hand, configure your HTTP client to auto-generate a UUID per logical request and attach it as Idempotency-Key on every POST/PUT/PATCH. Your business logic stays clean; the safety is automatic. The few cases where you need to control the key explicitly (cross-process retries) become easy to spot because they pass a key parameter.

09SDKs and Tooling

For most languages, the official SDK is the right choice. SDKs handle authentication, retries, rate limiting, pagination, error parsing, type definitions and version pinning — all the things you would otherwise build and maintain yourself.

What a good API SDK does for you

  • Auth — reads from env vars, attaches Authorization header, handles OAuth refresh if applicable.
  • Retries — exponential backoff with jitter, capped attempt counts, idempotency keys auto-attached on writes.
  • Rate limiting — respects 429 + Retry-After, optionally self-throttles using header signals.
  • Pagination — iterators that handle cursor-following automatically, so your code is just for call in client.calls.list().
  • Error parsing — turns the JSON error envelope into typed exception classes you can catch by category.
  • Types — in typed languages (TypeScript, Python with type hints, Go), full type definitions for every endpoint.
  • Version pinning — the SDK is built against a specific API version; upgrading the SDK is how you upgrade the version.
  • Telemetry — structured logging hooks, OpenTelemetry instrumentation, debug mode.

When to write your own HTTP client instead

Cases where the SDK is not the right answer:

  • No SDK exists in your language. Modern providers cover Node, Python, Ruby, PHP, Go, Java, .NET, often Swift and Kotlin; if you are working in something less common, you may have to roll your own.
  • The SDK is unmaintained. Last commit two years ago, abandoned issues, missing recent API features — safer to wrap a thin client yourself than depend on dead code.
  • You need behaviours the SDK doesn't expose. Custom transport (connection pool tuning, custom DNS), inspecting raw responses, request/response logging at low level, mocking for tests.
  • Bundle size matters. For browser code or edge runtimes (Cloudflare Workers, Deno Deploy), a 200KB SDK might be a non-starter when you only need three endpoints.
  • You have strict dependency policies. Some enterprises forbid pulling third-party packages; minimal HTTP fetch + manual JSON parsing is the only option.

Hybrid: use the SDK, drop down when needed

Most SDKs let you call arbitrary endpoints with raw HTTP for cases the typed methods do not cover. Use the SDK for everything common; drop to raw HTTP for the edge cases. You get the SDK's auth/retries/error-parsing for free even on raw calls, while not being limited to the SDK's surface area.

Stripe SDK example: typed methods plus raw call

// Typed: standard usage
const customer = await stripe.customers.create({ email: 'x@example.com' });

// Raw: when the SDK doesn't yet cover an endpoint
const response = await stripe.rawRequest('POST', '/v1/some-new-endpoint', {
  field: 'value'
});

Tooling that makes API integration easier

ToolPurposeNotes
curl / HTTPieOne-off requests from terminalHTTPie has nicer output; curl is everywhere
Postman / Insomnia / BrunoGUI HTTP clients with request collectionsPostman is full-featured but heavy; Bruno is open-source and offline-first
OpenAPI / SwaggerMachine-readable API specMany providers publish OpenAPI specs; can auto-generate client code
OpenAPI Generator / openapi-typescriptGenerate clients from OpenAPI specUseful when no SDK exists or the official SDK is weak
mitmproxy / Charles ProxyInspect HTTPS traffic between your code and the APIEssential for debugging weird behaviour; configure with care
VCR / nock / responsesRecord and replay HTTP interactions in testsYour test suite shouldn't hit the real API on every run

Team-Connect SDK availability

  • Node.js / TypeScriptnpm install @team-connect/node
  • Pythonpip install team-connect
  • Gogo get github.com/team-connect/go
  • PHPcomposer require team-connect/team-connect-php
  • Rubygem install team_connect
  • OpenAPI spec — published at /openapi.yaml for any language not covered by an official SDK.
Whichever path you choose, instrument it. SDK or hand-rolled, your API integration code should produce structured logs (method, URL, status, latency, request_id) and emit metrics (request rate, error rate, p95 latency per endpoint). The teams that catch API issues early have observability on every call; the teams that learn the hard way do not. Most modern SDKs ship with optional OpenTelemetry instrumentation — turn it on.

10Common API Issues and Troubleshooting

API integration failures cluster into about seven recurring patterns. Triage in this order.

401 Unauthorized on every request

Your API key is wrong, missing, or pointing at the wrong environment. Check: (1) Authorization header is exactly Bearer YOUR_KEY with one space — case matters, no quotes around the value; (2) the key is loaded from your env vars correctly — print its first 8 characters to confirm it's not undefined or empty; (3) you are using the right environment's key (test keys against test endpoints, production keys against production); (4) the key has not been revoked or rotated — check the dashboard.

403 Forbidden on a valid-looking request

Authentication works, but the key does not have permission for the action. Common causes: (1) key has read-only scope and you are trying to write; (2) key is restricted to certain resources and you are accessing one outside the allowlist; (3) account-level feature flag missing (e.g. accessing a beta endpoint without opt-in). Read the error body — it usually says which permission is missing.

5xx errors that come and go

Transient downstream issues. With proper retry logic (section 06), most 5xx are absorbed automatically. If your retries are exhausted, options: (1) increase retry count for non-critical paths; (2) check the provider's status page — an outage might be in progress; (3) capture request_ids and contact support if a specific endpoint is failing repeatedly. Persistent 5xx on a single endpoint while others work is provider-side and they need to fix it.

Silent timeouts (request hangs forever)

You did not set a timeout. The default for many HTTP libraries is "wait forever". Set timeout=30 (or shorter for user-facing flows) on every API call. After timeout, the right action depends on whether the request was idempotent — if you sent an idempotency key, retry safely; if not, you may have to read state from the API to figure out what actually happened.

Response parsing crashes when a field is missing

The API added/removed a field, or returns a different shape on edge cases (e.g. successful empty list returns []; failed empty list returns null). Defensive parsing: use .get('field', default) in Python, optional chaining (?.) in TypeScript, never assume nested fields exist. Wrap parsing in try/except and log the full payload on failures so you can diagnose schema surprises.

Pagination cuts off early or duplicates records

You are using offset pagination on a list that is changing during your iteration (section 05). Switch to cursor pagination if available, or freeze the data first (snapshot the table, iterate the snapshot). For "iterate everything" jobs, prefer endpoints designed for batch export over normal list endpoints — many providers have a separate bulk export with stronger consistency guarantees.

Test environment behaves differently from production

Common gotcha: test environments often have shorter rate limits, disabled features (no actual SMS delivery in test mode), or different idempotency key TTLs. Read the provider's "test mode differences" documentation — it exists, and reading it before debugging saves an afternoon. When in doubt, reproduce in production with a small but real call.

"Works on my laptop, fails in CI/production"

Environment difference, almost always. Check: (1) the API key is configured in CI secrets; (2) outbound HTTPS to the API is allowed from your CI/production network (firewalls, egress rules); (3) DNS resolves the API host (some private network setups block public DNS); (4) TLS certificate trust chain is current (containers with old CA bundles fail TLS verification on freshly-rotated certs). The classic shortcut to confirm network reachability is curl -v https://api.example.com/v1/health from within the failing environment.

Always include the request_id in support queries. Every reputable API returns a request_id on responses (often in a X-Request-Id header or in error envelopes). When you contact support, include it. The provider can look up the exact request, see what their server saw, and tell you precisely what went wrong — instead of guessing from your description. A support ticket with five request_ids gets resolved in minutes; one without takes hours.

API Documentation FAQs

The questions our customers ask most often when integrating Team-Connect's REST API into their systems.

What is a REST API?

A REST API (Representational State Transfer) is a way for two systems to communicate over HTTP using a small set of conventions: URLs identify resources, HTTP verbs (GET, POST, PUT, PATCH, DELETE) describe actions, status codes communicate outcomes, and JSON typically carries the data. Most modern web APIs are REST or REST-adjacent. The benefit is universality — any language with an HTTP client can call a REST API, and any developer who knows HTTP can read one with no extra learning.

How do I authenticate with a REST API?

The most common pattern in 2026 is bearer token authentication: include an HTTP header Authorization: Bearer YOUR_API_KEY on every request. The token is typically issued from the API provider's dashboard and treated as a long-lived secret. For more sensitive APIs, OAuth 2.0 with short-lived access tokens and refresh tokens is preferred. For server-to-server, mutual TLS (mTLS) adds another layer. Never send API keys in URL query parameters — they end up in server logs, browser history, and proxy caches.

What is API rate limiting?

Rate limiting is how an API enforces fair usage by capping how many requests a client can make in a time window — typically per minute or per hour. When you exceed the limit, the API returns HTTP 429 Too Many Requests, often with a Retry-After header indicating how long to wait. Well-behaved clients use exponential backoff with jitter when they hit 429. Most APIs also expose X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers on every response so you can self-throttle before hitting the cap.

What is the difference between cursor and offset pagination?

Offset pagination uses page numbers (page=2&limit=50) — simple but breaks when records are added or deleted between page loads, and gets dramatically slower on large datasets because the database has to count and skip rows. Cursor pagination uses an opaque token (cursor=eyJpZCI6MTIzfQ) representing the last item seen, returning items after that cursor — stable across data changes and consistently fast regardless of offset depth. Modern APIs strongly prefer cursor pagination for any list that might grow large or change frequently.

What is an idempotency key?

An idempotency key is a unique value you generate (typically a UUID) and include with write operations so the API can detect retries. If you call POST /v1/charges with the same Idempotency-Key twice, the API processes it once and returns the same response on the second call without re-charging. This makes write operations safe to retry after network failures — critical because you usually do not know whether a request that timed out was actually processed. Stripe, Square, Adyen and most modern payment APIs require idempotency keys on writes; many other APIs strongly recommend them.

How should APIs handle versioning?

Two main approaches. URL-based versioning puts the version in the path (/v1/calls, /v2/calls) — simple, visible, easy to route. Header-based versioning sends a version in a request header (Stripe-Version: 2026-04-01) — allows finer-grained versioning and per-request opt-in to changes. URL versioning is more common; header versioning is more flexible. Either way, breaking changes go in a new version, additive changes stay in the existing version, and providers should publish a deprecation policy with sunset dates. Never silently break existing API consumers.

What HTTP status codes should an API use?

200 OK for successful reads, 201 Created for successful writes that created a resource, 204 No Content for successful actions with no body to return. 400 Bad Request for malformed requests, 401 Unauthorized for missing or invalid auth, 403 Forbidden for valid auth without permission, 404 Not Found for missing resources, 409 Conflict for state conflicts, 422 Unprocessable Entity for validation failures, 429 Too Many Requests for rate limit hits. 500 Internal Server Error for server bugs, 502/503/504 for downstream or temporary unavailability. The single biggest mistake is using 200 OK for everything and putting error info in the response body — clients cannot distinguish success from failure without parsing the body, which breaks every standard HTTP tool.

Should I use the official SDK or write my own HTTP client?

Use the official SDK if one exists in your language. Modern API SDKs handle auth, retries, rate limiting, pagination, error parsing and typing for you — all the things you would otherwise have to build and maintain yourself. The remaining cases for writing your own HTTP client are: your language has no official SDK, the SDK is unmaintained, you need behaviours the SDK does not expose, or you have strict dependency constraints (browser bundle size, edge runtime). For most server-side code in Node, Python, Go, Ruby, PHP, Java, .NET, the official SDK is the right choice.

Continue Reading

The REST API is the request-response half of integration. To go deeper into the rest of the integration surface:

Webhook Integration → Voice Recognition Setup → TTS Configuration → SIP Protocol Basics → WebRTC Integration → AI Receptionist →