Skip to content

Quickstart

This walks from a fresh credential to a page of meetings. It takes two requests.

You need a client ID and client secret, issued by Salesbud for your company. Credentials are not self-service: ask your Salesbud contact, who will also confirm that the API_ACCESS feature is enabled for your company.

The secret is shown once, at creation. Store it in a secret manager — it cannot be read back, only rotated.

  1. Get an access token

    Tokens are issued through OAuth 2.0 client credentials. Send the credential as form-encoded fields:

    Terminal window
    curl -X POST https://api.salesbud.com.br/oauth/token \
    -d 'grant_type=client_credentials' \
    -d "client_id=$SALESBUD_CLIENT_ID" \
    -d "client_secret=$SALESBUD_CLIENT_SECRET"

    The endpoint requires application/x-www-form-urlencoded. Sending JSON returns 415 UNSUPPORTED_MEDIA_TYPE.

    200 OK
    {
    "access_token": "eyJhbGciOiJSUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "meetings.read calls.read"
    }
  2. Confirm what the token can do

    GET /v1/context reports the company, the scopes and the effective rate limit behind the token. It is the fastest way to tell a configuration problem from an integration bug.

    Terminal window
    curl https://api.salesbud.com.br/v1/context \
    -H "Authorization: Bearer $SALESBUD_ACCESS_TOKEN"
  3. Read a page of meetings

    Terminal window
    curl "https://api.salesbud.com.br/v1/meetings?meeting_after=2026-01-01T00:00:00Z&limit=50" \
    -H "Authorization: Bearer $SALESBUD_ACCESS_TOKEN"

    Only meetings your company owns and that finished processing are returned. See Meetings and calls for what “finished” means and why VoIP records live at /v1/calls instead.

  4. Walk the rest of the history

    Pass next_cursor back unchanged, keeping every filter identical, and loop on has_more — never on the size of data.

    let cursor = null;
    do {
    const url = new URL("https://api.salesbud.com.br/v1/meetings");
    url.searchParams.set("meeting_after", "2026-01-01T00:00:00Z");
    url.searchParams.set("limit", "50");
    if (cursor) url.searchParams.set("cursor", cursor);
    const page = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
    }).then((r) => r.json());
    for (const meeting of page.data) await handle(meeting);
    cursor = page.pagination.next_cursor;
    } while (cursor);

    A page can come back short — even empty — while has_more is true. That is expected: the service caps how much it scans per request. Pagination explains the guarantees.

There is no refresh token. Client credentials is a machine-to-machine grant with no user to re-consent, so RFC 6749 §4.4.3 says not to issue one. When the token expires, call /oauth/token again with the same credential.

In practice: cache the token for slightly less than expires_in, and re-issue on 401 INVALID_ACCESS_TOKEN rather than on a timer alone — a credential can be revoked before the clock runs out.

If the consumer is an agent rather than a service, you can skip the HTTP client entirely: the MCP server exposes these same operations as tools for Claude Desktop, Claude Code and Cursor, and takes care of token renewal and retries. You still need the credential above.

ResponseWhat it means
415 UNSUPPORTED_MEDIA_TYPEThe token request was sent as JSON. Use form encoding.
401 INVALID_CLIENTWrong client ID or secret, or the credential was revoked.
403 API_ACCESS_DISABLEDThe company does not have the API_ACCESS feature. Retrying will not help.
403 INSUFFICIENT_SCOPEThe token lacks a scope the route requires — calls.read for calls, and transcriptions.read on top of the collection scope for transcripts.
404 RESOURCE_NOT_FOUNDThe id does not exist, belongs to another company, or belongs to the other collection.