---
title: "Make your first API call"
description: "Go from API key to a working script that prints your sales pipeline — with stops for pagination, writes, and error handling along the way."
canonical_url: https://developer.onepagecrm.com/tutorials/first-api-call/
source: Markdown mirror of https://developer.onepagecrm.com/tutorials/first-api-call/
---

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`](https://requests.readthedocs.io/) for the final script.

## 1. Get your credentials

Sign in to OnePageCRM and open the API settings page:

[https://app.onepagecrm.com/app/api](https://app.onepagecrm.com/app/api)

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

| Value     | Used as             |
| --------- | ------------------- |
| `user_id` | HTTP Basic username |
| `api_key` | HTTP 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:

```bash
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:

```bash
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](#6-break-it-on-purpose) — that section
shows you exactly what a `401` looks like and how to fix it.

## 3. List your contacts

```bash
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:

```json
{
  "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:

| Parameter  | Default | Max |
| ---------- | ------- | --- |
| `per_page` | 10      | 100 |
| `page`     | 1       | —   |

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

```bash
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:

```bash
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:

```bash
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:

```json
{
  "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](/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](https://app.onepagecrm.com/app/api).

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

```bash
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:

```js
// 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}`);
```

```bash
node pipeline.mjs
```

**Python** — the same loop with `requests`:

```python
# 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)}")
```

```bash
python pipeline.py
```

Either way, the output is your live pipeline:

```text
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](/tutorials/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](/tutorials/webhooks/)** — get told
  when a deal changes instead of re-running the script.
- **[API reference](/api/reference/)** — every endpoint, parameter,
  and response shape, with try-it-now requests in the browser.
- **[Get started with OAuth](/oauth/quickstart/)** — when your
  integration needs to act on behalf of users other than yourself,
  swap Basic auth for OAuth 2.1.
