Skip to main content

Oppy API — getting started

Overview

Read and act on buying-signal cards from your own systems, all the way into your CRM. The API covers everything the dashboard does with cards: listing them, generating their narrative text, approving or rejecting them, revealing contacts and pushing them to Attio or HubSpot.

The base URL is:

https://api.oppyone.com/api/public/v1

Every endpoint except /health needs a key.

If you would rather have an AI agent do this work than write a client, see the MCP server — it exposes the same capabilities with the same permissions and costs.

Prerequisites

Before you start, you need:

  • An active Oppy account.
  • API access enabled for your workspace. This is switched on by Oppy — email contact@oppyone.com if you are not sure whether yours is. Without it every request returns not_found.
  • Admin permissions in Oppy — creating API keys is admin-only.
  • A connected CRM, if you plan to push cards into one. Connecting Attio or HubSpot needs a browser sign-in and cannot be done through this API; see the Attio and HubSpot guides.

Authentication

Create a key in the Oppy dashboard under Workspace settings → API keys. Copy it when it is shown — it is never displayed again.

Send it as either header:

Authorization: Bearer oppy_live_...
X-API-Key: oppy_live_...

A first request, to check everything works:

export OPPY_KEY="oppy_live_..."
export OPPY="https://api.oppyone.com/api/public/v1"

curl -s "$OPPY/workspace" -H "Authorization: Bearer $OPPY_KEY"
{
"id": "11111111-2222-3333-4444-555555555555",
"name": "your-workspace",
"website": "https://example.com",
"industry": "Software Development"
}
warning

Server-to-server only. Browsers are blocked deliberately — calling this from a web page would expose your key to anyone viewing the source.

Permissions (API key scopes)

Each key carries only the scopes you give it, so a reporting integration can hold a key that cannot spend credits or change anything.

ScopeGrants
cards:readList and read cards.
cards:writeHydrate, approve, reject, update.
contacts:readSee revealed contacts and candidates.
contacts:writeReveal contacts. Spends credits.
crm:writePush cards into your CRM. Writes to your system of record.
workspace:readWorkspace, users, credit balance.

crm:write is separate from cards:write on purpose. Approving a card changes something inside Oppy and can be undone; pushing writes records into your CRM where your whole team sees them and cleanup is manual.

The card lifecycle

A card starts as a shell and becomes an outreach-ready record in your CRM. Every step is optional — stop wherever it suits you.

  1. List cards in your inbox.
  2. Hydrate a card to generate its narrative text — skip if hydrated is already true.
  3. Approve it. This starts the contact lookup and spends nothing.
  4. List candidates to see who is available at that company.
  5. Reveal the one you want. This costs credits, and approves the card if it was still pending.
  6. Push to your CRM.

Rate limits

Counted per workspace, not per key — extra keys do not buy extra capacity, and the MCP server draws on the same budget.

Every response carries X-RateLimit-Limit, -Remaining and -Reset, so you can pace yourself instead of waiting to be rejected.

RequestsBurstSustainedWhy
Reads20 / sec120 / minDatabase only.
Writes10 / sec60 / minApprove, reject, patch.
Hydrate — pre-generated20 / sec120 / minNo model call needed.
Hydrate — needs generation2 / sec20 / minRoughly what the backend can serve.
Reveal5 / sec30 / minAlso bounded by your credit balance.
Push to CRM5 / sec30 / minProtects your own CRM's rate limit.
x-ratelimit-limit: 120 # the sustained window
x-ratelimit-remaining: 119
x-ratelimit-reset: 60 # seconds until capacity frees up

# on a 429, the window you actually breached:
retry-after: 1
x-ratelimit-limit: 20

Hydration has two speeds and you do not choose between them — Oppy decides per card. Watch X-RateLimit-Limit on the response to see which applied: 120 means the card was pre-generated, 20 means it cost a generation.

Responses and errors

Lists return data alongside pagination. Every non-2xx response — including a mistyped URL — returns the same envelope:

{
"error": {
"code": "not_found",
"message": "Card not found",
"request_id": "90cadb00-2fdd-4bbf-9f2a-1a5f0c3e8b21"
}
}

Quote the request_id when reporting a problem — it identifies the exact request in our logs.

CodeMeaning
400No CRM connected, or the card has no revealed contact.
401Key missing, unknown, revoked or expired. A revoked key can take up to 60s to stop working.
402Monthly reveal credits exhausted.
403Key is valid but lacks the required scope.
404Not found, or belongs to another workspace — deliberately indistinguishable.
409Not possible for this card, e.g. a reveal with no candidate_id.
422Invalid parameter, status, assignee email, or limit above 100.
429Rate limited. See Retry-After.
502Hydration, reveal, or every CRM push failed downstream. Safe to retry.

A complete run

export OPPY="https://api.oppyone.com/api/public/v1"
export H="Authorization: Bearer $OPPY_KEY"

# 1. the inbox — best cards first
CARD=$(curl -s "$OPPY/cards?card_status=pending&limit=1" -H "$H" | jq -r '.data[0].id')

# 2. narrative text (skip if .hydrated is already true)
curl -s -X POST "$OPPY/cards/$CARD/hydrate" -H "$H" | jq '{hydrated, strategy_text}'

# 3. approve — starts the contact lookup, costs nothing
curl -s -X POST "$OPPY/cards/$CARD/approve" -H "$H" | jq '.card_status'

# 4. who is available (free) — may be empty for ~a minute after approving
CAND=$(curl -s "$OPPY/cards/$CARD/contact-candidates" -H "$H" | jq -r '.[0].id')

# 5. reveal that person — 1 credit, and approves the card if still pending
curl -s -X POST "$OPPY/cards/$CARD/contacts/reveal" -H "$H" \
-H "Content-Type: application/json" \
-d "{\"candidate_id\": \"$CAND\"}" | jq '{contact: .contact.email, credits_used}'

# 6. into the CRM
curl -s -X POST "$OPPY/cards/$CARD/push-to-crm" -H "$H" | jq '.results[] | {provider, status}'

Support & Troubleshooting