Tutorials menu

Tutorial · Get started

Make your first API call

Go from API key to a working script that prints your sales pipeline — with stops for pagination, writes, and error handling along the way.

Last updated Jul 14, 2026

By the end of this tutorial you’ll have a small script that pages through your deals and prints your open pipeline with a total. You’ll get there in steps that each teach one thing: prove your credentials work, read the response envelope, page through a list, create a record, and see what errors actually look like.

Everything up to the final script is plain curl, so you can follow along in any terminal. The script itself comes in Node 22 and Python flavors — pick one.

What you’ll need

  • A OnePageCRM account you can sign in to.
  • curl.
  • Node 22 or Python 3 with requests for the final script.

1. Get your credentials

Sign in to OnePageCRM and open the API settings page:

https://app.onepagecrm.com/app/api

On the Configuration tab you’ll find the two values the API uses for HTTP Basic auth:

ValueUsed as
user_idHTTP Basic username
api_keyHTTP Basic password

The api_key grants full access to your account — treat it like a password. Export both as environment variables so they never end up in your shell history or your code:

export ONEPAGECRM_USER_ID="your-user-id"
export ONEPAGECRM_API_KEY="your-api-key"

2. Prove they work

The base URL for the REST API is https://app.onepagecrm.com/api/v3. The best smoke test is GET /bootstrap.json — it returns account-wide reference data (statuses, deal stages, custom field schemas) and confirms your credentials in one call:

curl -u "$ONEPAGECRM_USER_ID:$ONEPAGECRM_API_KEY" \
  https://app.onepagecrm.com/api/v3/bootstrap.json

If you get JSON back, you’re authenticated. If you get a 401, jump ahead to break it on purpose — that section shows you exactly what a 401 looks like and how to fix it.

3. List your contacts

curl -u "$ONEPAGECRM_USER_ID:$ONEPAGECRM_API_KEY" \
  https://app.onepagecrm.com/api/v3/contacts.json

Every response — this one and every other — arrives in the same envelope:

{
  "status": 0,
  "message": "OK",
  "timestamp": 1781100000,
  "data": {
    "contacts": [
      { "id": "5f...", "first_name": "Ada", "last_name": "Lovelace", "...": "..." }
    ],
    "total_count": 34,
    "page": 1,
    "per_page": 10,
    "max_page": 4
  }
}

Three things to internalize now, because every later step relies on them:

  • status: 0 means success. Non-zero means an error.
  • data carries the payload, shaped per endpoint.
  • List endpoints add pagination metadata: total_count, page, per_page, max_page.

4. Page through

In the response above, total_count is 34 but only 10 contacts came back. That’s the default page size. Pagination is controlled with two query parameters:

ParameterDefaultMax
per_page10100
page1

So to fetch everything with the fewest requests, ask for 100 per page and walk page from 1 to max_page:

curl -u "$ONEPAGECRM_USER_ID:$ONEPAGECRM_API_KEY" \
  "https://app.onepagecrm.com/api/v3/contacts.json?page=2&per_page=100"

When page equals max_page, you’ve seen everything. That loop — fetch, append, check max_page — is the heart of the final script.

5. Create a contact

Reads use GET; writes use POST with a JSON body:

curl -u "$ONEPAGECRM_USER_ID:$ONEPAGECRM_API_KEY" \
  -X POST https://app.onepagecrm.com/api/v3/contacts.json \
  -H "Content-Type: application/json" \
  -d '{"first_name": "Ada", "last_name": "Lovelace"}'

The response uses the same envelope, and data echoes the new contact back — including the id the server assigned. Save that id if you want to update or delete the record later. (Ada is now a real contact in your account — feel free to delete her in the app when you’re done.)

6. Break it on purpose

You’ll hit errors in real integrations, so let’s meet the two most common ones now, while the stakes are zero.

A 401 — bad credentials. Re-run the contacts call with a deliberately wrong key:

curl -i -u "$ONEPAGECRM_USER_ID:wrong-key" \
  https://app.onepagecrm.com/api/v3/contacts.json

The -i flag shows the HTTP status line: 401 Unauthorized. The body carries five fields:

{
  "status": 400,
  "message": "Invalid auth token",
  "error_name": "invalid_auth_token",
  "error_message": "Authorization token is invalid",
  "errors": {}
}

Notice the body says "status": 400 while the HTTP status line says 401. The body’s status is an API error code, and it can differ from the HTTP status — trust the HTTP status line for control flow. Of the five fields, error_name is the stable identifier to branch on in code, error_message is the human-readable text to log, and errors holds per-field details on validation failures. The full catalog is on API errors.

When you see a 401, the fix is almost always the same: re-copy user_id and api_key from the API settings page.

A validation error — bad write. Now send a contact with no fields at all:

curl -i -u "$ONEPAGECRM_USER_ID:$ONEPAGECRM_API_KEY" \
  -X POST https://app.onepagecrm.com/api/v3/contacts.json \
  -H "Content-Type: application/json" \
  -d '{}'

The request is well-formed and authenticated, but the payload fails validation, so the API rejects it — again with error_name and error_message telling you what to fix. The lesson for your code: check the HTTP status, and when it’s not a 2xx, surface error_message instead of swallowing the body.

7. The script: print your pipeline

Time to assemble the pieces. The script below pages through GET /deals.json (the same loop from step 4, same envelope from step 3, same error handling from step 6), keeps the deals whose status is pending, and prints them with a total.

Node 22 — save as pipeline.mjs, no dependencies needed:

// pipeline.mjs — prints every open deal in your pipeline
const { ONEPAGECRM_USER_ID: user, ONEPAGECRM_API_KEY: key } = process.env;
const auth = "Basic " + Buffer.from(`${user}:${key}`).toString("base64");
const base = "https://app.onepagecrm.com/api/v3";

async function getPage(page) {
  const res = await fetch(`${base}/deals.json?page=${page}&per_page=100`, {
    headers: { Authorization: auth },
  });
  const body = await res.json();
  if (!res.ok) {
    throw new Error(
      `HTTP ${res.status}: ${body.error_name}${body.error_message}`,
    );
  }
  return body.data;
}

const deals = [];
let page = 1;
let maxPage = 1;
do {
  const data = await getPage(page);
  deals.push(...data.deals);
  maxPage = data.max_page;
  page += 1;
} while (page <= maxPage);

const open = deals.filter((d) => d.status === "pending");
for (const d of open) {
  console.log(`${d.name.padEnd(40)} ${d.amount}`);
}
const total = open.reduce((sum, d) => sum + (d.amount ?? 0), 0);
console.log(`\n${open.length} open deals worth ${total}`);
node pipeline.mjs

Python — the same loop with requests:

# pipeline.py — prints every open deal in your pipeline
import os
import requests

auth = (os.environ["ONEPAGECRM_USER_ID"], os.environ["ONEPAGECRM_API_KEY"])
base = "https://app.onepagecrm.com/api/v3"

deals, page, max_page = [], 1, 1
while page <= max_page:
    res = requests.get(
        f"{base}/deals.json",
        auth=auth,
        params={"page": page, "per_page": 100},
    )
    body = res.json()
    if not res.ok:
        raise SystemExit(
            f"HTTP {res.status_code}: {body.get('error_name')}{body.get('error_message')}"
        )
    deals += body["data"]["deals"]
    max_page = body["data"]["max_page"]
    page += 1

open_deals = [d for d in deals if d["status"] == "pending"]
for d in open_deals:
    print(f"{d['name']:<40} {d['amount']}")
print(f"\n{len(open_deals)} open deals worth {sum(d['amount'] or 0 for d in open_deals)}")
python pipeline.py

Either way, the output is your live pipeline:

Acme renewal                             12000
Globex onboarding                        8500
Initech expansion                        20000

3 open deals worth 40500

If it prints nothing, you have no pending deals — create one in the app and run it again.

Where to go next

  • Query CRM data with OQL — the whole script above collapses into one JSON query (from: deals, where: { status: "pending" }), with sorting and aggregates built in — via the MCP server.
  • Subscribe to webhook events — get told when a deal changes instead of re-running the script.
  • API reference — every endpoint, parameter, and response shape, with try-it-now requests in the browser.
  • Get started with OAuth — when your integration needs to act on behalf of users other than yourself, swap Basic auth for OAuth 2.1.