Skip to main content
AVnesterDevelopers

Server credentials · Business and above

Server credentials for server-to-server calls

A server credential lets your backend call the same public tools with no person signed in: a nightly sync, a pricing engine, a CRM enrichment job. It identifies your application, not a user, and every call it makes is counted against that application’s allowance. The simplest is an API key; OAuth client credentials (a signed key or a client secret, exchanged for short-lived tokens) suit teams whose security policy asks for OAuth. Server credentials come with Business, Scale and Enterprise.

1 · Choosing

Which credential should I use?

  • Your server calls on its own schedule, and no user signs in. Use a server credential, below.
  • A person connects their own account (an AI assistant, a connector, an app where each user authorises): use OAuth, so each call is made on behalf of that person.
  • You are trying the API. A call made without signing in needs nothing at all; see the quickstart.

Which kind of server credential?

API key (avn_live_…)
One secret, sent as a Bearer header on every request. Nothing to exchange or refresh. The simplest choice, and the right one unless your security policy asks for OAuth.
Signed key (OAuth client credentials, private_key_jwt)
Your server keeps a private key, ideally in a key vault or HSM, and signs a short-lived assertion each time it needs a token. We hold only the public key, so there is no shared secret to leak, and the key can be rotated without a new client ID. Recommended where your security team manages keys.
Client secret (OAuth client credentials, client_secret_basic)
A client ID and secret your OAuth library exchanges for short-lived tokens. Choose it when your tooling expects the standard client-credentials grant and cannot sign assertions.

Availability. API keys can be created today. OAuth client credentials are issued once the platform enables them: from then on the console’s method picker lists Signed key and Client secret beside API key, and the authorization server’s discovery document, https://auth.avnester.com/.well-known/oauth-authorization-server, lists client_credentials in grant_types_supported. Until then the console creates API keys only.

2 · Setup

How do I create one?

  1. Open your app in the developer console. The app must be on Business, Scale or Enterprise, its ownership must be verified, and your account needs a verified email address.
  2. Under Server credentials, choose Create API key (or Create credential once the console offers more than one kind, then pick the kind). Give it a label you will recognise later (the service or environment that will use it), pick its scopes, and optionally add an IP allowlist or a shorter lifetime. For a signed key, paste its public key: a PEM (-----BEGIN PUBLIC KEY-----) or a JWK, EC P-256 or RSA of 2048 bits or more.
  3. Copy an API key or a client secret straight into your secrets manager. It is shown once. We keep only a hash of an API key and its last four characters, so it cannot be shown again: if it is lost, create a new credential and revoke the old one. A client secret is never shown again either. A signed key shows nothing secret: check that the thumbprint shown matches your key, and revoke the credential if it does not.

A Developer app cannot hold a server credential: it is an evaluation tier with no commercial licence, and a server credential is how a product ships. See pricing, or write to partnerships@avnester.com to upgrade an existing app. The upgrade keeps the app and its OAuth client.

3 · The key

How do I send an API key?

Send it in the Authorization header with the Bearer scheme, on every request. There is no token exchange and nothing to refresh: the same key works until you revoke it or it expires.

# call estimate_stamp_duty_and_registration with a server API key
# AVNESTER_API_KEY holds the key (avn_live_…), injected from your secrets manager.
curl -X POST https://api.avnester.com/public/v1/estimate_stamp_duty_and_registration \
  -H "Authorization: Bearer $AVNESTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"propertyValue":6500000,"state":"TN","buyerGender":"female"}'

Where to keep it

  • In a secrets manager (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, or your platform’s encrypted environment variables), injected into the process at deploy time.
  • Never in source control, a container image, a log line, or a support ticket.
  • Never in a browser page or a mobile app. Anything shipped to a device can be read off it, and a browser request from another site is refused anyway (see what a credential cannot do).
  • One credential per service or environment, each with its own label, so a leak can be revoked without taking everything else down.

The same rules hold for a client secret and for a signed key’s private key. To limit where a credential works, add an IP allowlist when you create it.

4 · Tokens

How do I get a token with OAuth client credentials?

An OAuth credential is never sent on a tool call itself. Your server exchanges it at the token endpoint for a short-lived access token (the client-credentials grant, RFC 6749 §4.4), then sends that token as a Bearer header exactly where an API key would go. This works once OAuth client credentials are enabled (see availability).

POST https://auth.avnester.com/oauth/token, form-encoded:

  • grant_type=client_credentials
  • scope: one or more of the credential’s scopes, space-separated. Send it on every token request; asking for a scope the credential does not carry is HTTP 400 invalid_scope, whose scope member names the ones refused.
  • resource (optional): if you send it, it must be the public MCP server, https://mcp.avnester.com/public-mcp. Anything else is HTTP 400 invalid_target.
  • Client authentication: HTTP Basic for a client secret; a signed client assertion for a signed key.

The answer is JSON. The access token is a JWT; treat it as opaque.

# a token response
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9…",
  "expires_in": 900,
  "token_type": "Bearer",
  "scope": "calculators:use"
}

A token lasts 15 minutes (expires_in is in seconds). Reuse it for every call until 60 seconds before it expires, then request the next one (see caching).

With a client secret

Send the client ID and secret with HTTP Basic authentication (client_secret_basic), read from your secrets manager, never typed into a command:

# request a token with a client secret, then call a tool
# AVNESTER_CLIENT_ID and AVNESTER_CLIENT_SECRET come from your secrets manager.
# 1. Request a token. Reuse it until a minute before it expires.
TOKEN=$(curl -s -X POST https://auth.avnester.com/oauth/token \
  -u "$AVNESTER_CLIENT_ID:$AVNESTER_CLIENT_SECRET" \
  -d grant_type=client_credentials \
  --data-urlencode "scope=calculators:use" | jq -r .access_token)

# 2. Call a tool with it, exactly where an API key would go.
curl -X POST https://api.avnester.com/public/v1/estimate_stamp_duty_and_registration \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"propertyValue":6500000,"state":"TN","buyerGender":"female"}'

With a signed key

Sign a short JWT, the client assertion (RFC 7523), with your private key, and send it in the token request as client_assertion with client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer (private_key_jwt). What we check:

  • alg: ES256 for an EC P-256 key, RS256 or PS256 for an RSA key, signed with the private half of a public key registered on the credential.
  • iss and sub: your client ID.
  • aud: the token endpoint, https://auth.avnester.com/oauth/token, or the issuer, https://auth.avnester.com.
  • jti: unique for every assertion. An assertion is single-use; one whose jti we have already seen is refused.
  • iat: the time you signed it. It must be present, and not in the future.
  • exp: at most 300 seconds after iat.
# sign a client assertion with jose, request a token, reuse it, call a tool
// Node 18+ with `npm install jose`. The private key never leaves your server:
// AVNESTER_PRIVATE_KEY holds it (PKCS#8 PEM) and AVNESTER_CLIENT_ID the client ID.
import { SignJWT, importPKCS8 } from 'jose';
import { randomUUID } from 'node:crypto';

const tokenEndpoint = 'https://auth.avnester.com/oauth/token';
const clientId = process.env.AVNESTER_CLIENT_ID;
const alg = 'ES256'; // your key's algorithm: ES256 (EC P-256), or RS256 / PS256 (RSA)
const privateKey = await importPKCS8(process.env.AVNESTER_PRIVATE_KEY, alg);

let cached = null; // { token, expiresAt } — one token serves many calls

async function accessToken() {
  const now = Math.floor(Date.now() / 1000);
  if (cached && cached.expiresAt - 60 > now) return cached.token;
  const assertion = await new SignJWT({})
    .setProtectedHeader({ alg })
    .setIssuer(clientId)
    .setSubject(clientId)
    .setAudience(tokenEndpoint)
    .setJti(randomUUID()) // single-use
    .setIssuedAt(now)
    .setExpirationTime(now + 60)
    .sign(privateKey);
  const res = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      scope: 'calculators:use',
      client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
      client_assertion: assertion,
    }),
  });
  if (!res.ok) throw new Error(`token request failed: ${res.status} ${await res.text()}`);
  const body = await res.json();
  cached = { token: body.access_token, expiresAt: now + body.expires_in };
  return cached.token;
}

const answer = await fetch('https://api.avnester.com/public/v1/estimate_stamp_duty_and_registration', {
  method: 'POST',
  headers: { Authorization: `Bearer ${await accessToken()}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"propertyValue":6500000,"state":"TN","buyerGender":"female"}),
});

Other languages sign the same claims. Python: PyJWT (jwt.encode(claims, key, algorithm="ES256")) with requests, or Authlib’s PrivateKeyJWT client authentication. Java: the Nimbus OAuth 2.0 SDK (PrivateKeyJWT with a ClientCredentialsGrant). .NET: Duende IdentityModel’s RequestClientCredentialsTokenAsync with a ClientAssertion. Whatever signs it, the claims above are what we check.

5 · Calling

Which endpoints accept a key or a token?

Both public transports, with the same tools, the same inputs and the same answers as every other caller, unless your app has a data contract. An OAuth access token goes in the same header as a key, on the same endpoints, and is counted the same way: against your application. The REST adapter takes the tool name in the path:

POST https://api.avnester.com/public/v1/<tool> · every tool · OpenAPI document

# the same call from a Node service
// Node 18+ — the key is read from the environment your secrets manager fills.
const res = await fetch('https://api.avnester.com/public/v1/estimate_stamp_duty_and_registration', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.AVNESTER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"propertyValue":6500000,"state":"TN","buyerGender":"female"}),
});

if (res.status === 503) {
  // credential_check_unavailable: we could not confirm the key. Retry with
  // backoff. The key is fine — do not rotate it.
}
const answer = await res.json();

The MCP endpoint takes a JSON-RPC tools/call with the same header. It is stateless, so a single request is enough:

# the same call over MCP
curl -X POST https://mcp.avnester.com/public-mcp \
  -H "Authorization: Bearer $AVNESTER_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"estimate_stamp_duty_and_registration","arguments":{"propertyValue":6500000,"state":"TN","buyerGender":"female"}}}'

6 · Scopes

What can a credential call?

A credential carries the scopes you choose when you create it, and a tool whose scope it lacks is refused with HTTP 403 naming the missing scope. An OAuth token carries the scopes its token request asked for, each of which must be on the credential. Pick only the scopes the service needs; to change them, create a new credential and revoke the old one.

calculators:use
Deterministic financial calculators: affordability, loans, tax regime, stamp duty, and the decision summary. Tools: calculate_home_affordability, compare_balance_transfer, estimate_stamp_duty_and_registration, get_property_decision_intelligence, optimize_home_loan_tax_regime, simulate_loan_prepayment.
listings:read:public
Publicly available property listings, never seller contact details. Tools: compare_properties, get_property_details, search_properties.
locality:read:public
Aggregated locality insights and comparisons. Tools: compare_localities, get_locality_insights.

7 · Caching

Should I cache answers and tokens?

Answers: yes, for as long as the answer is useful to you. A repeated query is served from our cache, but it still counts toward your allowance: you asked for the data and got it. Keeping the answer on your side is the cheapest call there is.

Tokens: always. An OAuth access token lasts 15 minutes. Request one, reuse it for every call until 60 seconds before its exp (or before expires_in runs out), then request the next. A token request per call adds a round trip to every call, and a signed key would sign an assertion for each one.

An API key has no token to cache. The key itself is the credential, sent as-is on every request.

8 · Rotation

How do I rotate or revoke a credential?

An app can hold a second active credential, of any kind, so rotation needs no downtime:

  1. Create a new credential in the console.
  2. Deploy it everywhere the old one is used.
  3. Watch the old one’s last used time in the console (for an OAuth credential, last token issued) stop moving. It is updated every few minutes, not on every call.
  4. Revoke the old credential.

A signed key can also rotate its key without a new client ID: the credential holds more than one public key at a time. Add the new public key to the credential in the console, deploy the new private key, confirm tokens are issued, then remove the old key. A client secret cannot be changed or shown again, so it rotates by creating a new credential.

  • Revocation. A revoked API key is refused within a minute on every server. A revoked OAuth credential gets no new tokens from that moment, and the tokens it already holds stop working within 15 minutes, when they expire. Either way, the licensed cities of a data contract stop on its next call. Neither can be undone.
  • Expiry. An API key and a client secret expire one year after they are created, a signed key two years after, unless you chose a shorter lifetime. The console shows each credential’s expiry date, and the app’s owner is emailed a month before it and again a week before it; rotate before it.
  • If a credential leaks, revoke it first and rotate second. A revoked key is refused within a minute and a revoked OAuth credential gets no new token at all; the new credential can follow at deploy speed.
  • Account suspension. If the account that owns the app is disabled, its credentials are refused until it is re-enabled, token requests included. They are not revoked. The same holds if the app moves to a tier without server credentials: they work again when it moves back.

9 · Network

Can I restrict a credential to my servers?

Yes. When you create a credential, list the IPv4 or IPv6 addresses, or CIDR ranges, your servers call from. A request from any other address is refused with HTTP 403 ip_not_allowed. The list is optional: an empty one accepts any address, the licensed cities of a data contract included, so a leaked credential works from anywhere until you revoke it.

For an OAuth credential the list is checked twice: at the token endpoint (a token request from another address is HTTP 400 unauthorized_client with ip_not_allowed), and on every call made with its token, because a token can travel.

List your public egress address (a NAT gateway’s or a proxy’s), not a private network address: the address we check is the one that reaches us. The allowlist is fixed for the life of the credential; to change it, create a new credential with the new list and revoke the old one.

10 · Token binding

Can I bind a credential to a keypair?

An API key, no. It is a bearer credential, sent with the Bearer scheme. A key sent with the DPoP scheme is refused with HTTP 401 invalid_auth_header. Its protection is the secrets manager, the IP allowlist and prompt revocation.

An OAuth credential, if you choose to. A credential can be DPoP-bound: tick Bind tokens to a DPoP key under Advanced when you create it. Every token it then gets is sender-constrained (RFC 9449): send a DPoP proof signed by your keypair with each token request and each call, using the DPoP authorization scheme, and a stolen token is useless without your keypair.

Binding is an opt-in per credential, and an optional uplift across the platform: a credential that is not bound uses plain Bearer tokens. Leave it off unless your HTTP client can make DPoP proofs. The same uplift exists on the end-user OAuth flow.

11 · Errors

What errors can a credential get?

Every REST error body is JSON with an error code and a readable message. Every 429 also carries a reason naming the limit that refused it, and a Retry-After header. Branch on error and reason, never the message.

With an API key

  • 401invalid_api_key

    The key is malformed, unknown or revoked.

    Check that the whole key was sent. If it was revoked, use its replacement.

  • 401api_key_expired

    The key is past its expiry date.

    Create a new key, deploy it, and revoke the expired one.

  • 401invalid_auth_header

    The key was sent with a scheme other than Bearer (for example DPoP).

    Send Authorization: Bearer <key>.

  • 403app_inactive

    The app the key belongs to is not active (it was revoked).

    Keys die with their app. Create a new app and key in the console.

  • 403app_claim_unproved

    The app’s ownership was never verified.

    Re-link the app in the console to verify it.

  • 403tier_not_licensed

    The app is on a tier without server credentials. They come with Business, Scale and Enterprise.

    The key is kept and works again once the app is back on a licensed tier.

  • 403owner_suspended

    The account that owns the app is disabled.

    Contact partnerships@avnester.com.

  • 403ip_not_allowed

    The request came from an address outside the key’s IP allowlist.

    Call from a listed address, or create a key whose list includes this one.

  • 503credential_check_unavailable

    We could not confirm the key just now.

    Retry with backoff. The key is fine: do not rotate it.

At the token endpoint

The token endpoint answers the OAuth way (RFC 6749 §5.2): JSON with error and error_description. A refusal about the credential or its app is HTTP 400 unauthorized_client, with the reason in error_description; branch on both.

  • 400unauthorized_clienterror_description: "credential_inactive"

    The credential is revoked. A revoke also deletes its OAuth client at once, so most requests with a revoked credential get 401 invalid_client instead.

    Use its replacement, or create a new credential.

  • 400unauthorized_clienterror_description: "credential_expired"

    The credential is past its expiry date. 7 days after expiry its OAuth client is deleted, and requests then get 401 invalid_client.

    Create a new credential, deploy it, and revoke the expired one.

  • 400unauthorized_clienterror_description: "app_inactive"

    The app the credential belongs to is revoked.

    Credentials die with their app. Create a new app and credential in the console.

  • 400unauthorized_clienterror_description: "app_claim_unproved"

    The app’s ownership was never verified.

    Re-link the app in the console to verify it.

  • 400unauthorized_clienterror_description: "tier_not_licensed"

    The app is on a tier without server credentials. They come with Business, Scale and Enterprise.

    The credential is kept and gets tokens again once the app is back on a licensed tier.

  • 400unauthorized_clienterror_description: "owner_suspended"

    The account that owns the app is disabled.

    Contact partnerships@avnester.com.

  • 400unauthorized_clienterror_description: "ip_not_allowed"

    The token request came from an address outside the credential’s IP allowlist.

    Request tokens from a listed address, or create a credential whose list includes this one.

  • 400unauthorized_clienterror_description: "client_auth_required"

    The client ID belongs to a client that does not authenticate (an end-user OAuth client).

    Use the client ID of a server credential from the console, with its secret or signed key.

  • 400invalid_scope

    No scope was sent, or a scope was asked for that the credential does not carry. The response’s scope member names the refused ones.

    Ask for one or more of the scopes on the credential.

  • 400invalid_target

    The resource parameter names something other than the public MCP server.

    Omit resource, or send https://mcp.avnester.com/public-mcp.

  • 401invalid_client

    Client authentication failed: a wrong ID or secret, a revoked credential, one more than 7 days past its expiry, or a client assertion that is mis-signed, expired, reused or addressed to the wrong audience. The description is always “client authentication failed”, so it cannot be used to probe a credential.

    Check the client ID and secret, or the assertion’s claims, key and clock. A revoked or long-expired credential needs a new one from the console.

  • 500server_error

    We could not check the credential just now.

    Retry with backoff. The credential is fine: do not rotate it.

Calling with a token

  • 401invalid_token

    The access token is expired, malformed or was not issued by us.

    Request a new token. Reuse a token until a minute before it expires, never past it.

  • 403ip_not_allowed

    The call came from an address outside the credential’s IP allowlist, which the token carries.

    Call from a listed address.

  • 401dpop_required

    The credential is DPoP-bound and the call used the Bearer scheme.

    Send Authorization: DPoP <token> with a DPoP proof header (dpop_missing and invalid_dpop_proof mean the proof was absent or did not verify).

A tool the credential has no scope for is a 403 naming the missing scope, exactly as for OAuth callers.

Rate limits: HTTP 429

Every limit answers with HTTP 429, error: "rate_limited" and one of these reason values. Retry-After is the number of seconds to wait. Over MCP the same refusal arrives as a tool error whose message names the limit.

reason: "rate_limit_per_tool"
the per-minute rate for one tool. Retry-After: seconds until that per-minute window resets.
reason: "anon_aggregate"
requests per minute from one IP across every public tool, for calls made without signing in. Retry-After: seconds until that per-minute window resets.
reason: "anon_daily"
tool calls in a UTC day from one IP, for calls made without signing in. Retry-After: seconds until 00:00 UTC.
reason: "cache_miss_budget"
distinct (uncached) queries per minute for one tool, for calls made without signing in. Retry-After: seconds until that per-minute window resets.
reason: "locality_cardinality"
distinct localities queried in a UTC day. Retry-After: seconds until 00:00 UTC.
reason: "usage_daily"
the fair-use daily line, on a tier that refuses at its allowance. Retry-After: seconds until 00:00 UTC.
reason: "usage_month"
the monthly allowance, on a tier that refuses at it. Retry-After: seconds until the monthly allowance resets.
reason: "usage_ceiling"
this month’s ceiling, on a tier that bills overage. Retry-After: seconds until the monthly allowance resets.

12 · Limits

What are the limits for a credential?

Limits belong to the application, not to the credential: two credentials on one app share its limits, whatever their kind, and calls your users make through the same app’s OAuth flow count toward the same monthly allowance. The per-minute rate applies per application, per tool.

Business
10,000 calls / month included · 600 / min per tool
then ₹0.45 per call + GST, up to 30,000 calls / month
Scale
50,000 calls / month included · 1,200 / min per tool
then ₹0.30 per call + GST, up to 1,50,000 calls / month
Enterprise
No monthly cap — fair use · 6,000 / min per tool
Monthly volume and anything above it are set by your contract.

On Business and Scale, a call above the monthly allowance is served and billed at the tier’s per-call rate, plus GST, invoiced with the month. Calls stop at the ceiling, 3× the allowance, with HTTP 429, error: "rate_limited" and reason: "usage_ceiling" on REST (over MCP, a tool error whose message names the ceiling and the reset date), until the month resets. The ceiling can be raised for an app by contract. The billing month is the UTC calendar month, the same window the headers and the console count.

Usage alerts

On Business and Scale, we email the app’s owner at their verified address, and a billing contact if we have one on file for the app: as the app nears its monthly allowance, when it passes the allowance and overage billing starts, and when it reaches the ceiling. Each goes out once a month. A day well above the fair-use daily figure (a tenth of the allowance) gets a heads-up too, at most once a day.

Usage headers

Every REST response carries the application’s monthly position: X-AVnester-Usage-Limit (the allowance), X-AVnester-Usage-Remaining (calls left inside it, never below zero) and X-AVnester-Usage-Reset, plus the day’s fair-use line in X-AVnester-Usage-Daily-Limit and X-AVnester-Usage-Daily-Remaining. On a tier that bills overage the daily line is advisory: calls are still served, and X-AVnester-Usage-Daily-Remaining: 0 does not mean the next call is refused. Two more headers say where you are past the allowance: X-AVnester-Usage-Overage (calls above it this month) and X-AVnester-Usage-Ceiling (where calls stop). The IETF RateLimit-Policy and RateLimit headers carry the per-minute budget. On MCP, the allowance headers reflect the start of the request.

# response headers, past the allowance
# Business app, part-way through a month past its allowance
X-AVnester-Usage-Limit: 10000
X-AVnester-Usage-Remaining: 0
X-AVnester-Usage-Reset: 2026-10-01T00:00:00.000Z
X-AVnester-Usage-Daily-Limit: 1000
X-AVnester-Usage-Daily-Remaining: 0
X-AVnester-Usage-Overage: 1250
X-AVnester-Usage-Ceiling: 30000

Full tier terms are on the pricing page.

13 · Boundaries

What can a server credential not do?

  • Talk to Ava. The assistant’s agent endpoint (/a2a) serves people, and refuses a server key or a server credential’s token with HTTP 403 client_token_not_a_user.
  • Reach anything but the public tools. AVnester’s internal endpoints refuse a key or a server token (HTTP 401).
  • Run in a browser. A browser request carrying a key and another site’s origin is refused (HTTP 403). Server credentials are for servers: a key, a client secret or a private key shipped to a device can be read off it. An app used by people in a browser should use OAuth.
  • Act as a person. A server credential has no user behind it, so nothing it does is tied to anyone’s account or history.
  • Receive licensed cities on its own. A server credential receives the public data. Licensed cities under a data contract are served to a server credential only when the contract allows server access and the key holds locality:read:public. An IP allowlist is optional: a key with one is served only from those addresses. Revoking the key stops licensed answers on its next call. When an app’s contract allows server access, the console lists the licensed cities being served and marks each key that receives them, or the scope it lacks. Write to partnerships@avnester.com about a data contract.