Upstream

Upstream CRM API

Version 1 · REST · JSON

A JSON REST API for managing pipelines, deals, contacts, activities and activity templates inside an Upstream organization. Designed for the official Upstream mobile app and any third-party integration you want to build on top of your data.

JWT auth

15-minute access tokens, rotating refresh tokens with reuse detection.

Multi-tenant safe

Every request is scoped to the organization in your token — never trust the body.

Rate-limited

Per-IP and per-user limits on sensitive routes (login, refresh).

Versioned

All endpoints live under /api/v1. Breaking changes go in /api/v2.

Base URL
https://app.upstreamcrm.com/api/v1
OpenAPI 3.1 spec

Machine-readable contract. Import into Postman, Insomnia, or generate clients with openapi-generator.

Download openapi.json

Authentication

All endpoints (except /auth/*) require a Bearer access token in the Authorization header. Tokens are short-lived (15 minutes). When an access token expires, exchange your refresh token at POST /auth/refresh for a new pair. Refresh tokens are single-use; replaying one revokes every active session for that user.

http
GET /api/v1/me HTTP/1.1
Host: app.upstreamcrm.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIi...
Accept: application/json
Token storage: on mobile, store both tokens in the platform secure store (iOS Keychain via Expo SecureStore / Android Keystore). Never persist them in plain AsyncStorage or localStorage.

Response envelope

Every response is a JSON object with a success boolean. Successful responses include a data field; errors include error, code and an optional details payload.

Success
json
{
  "success": true,
  "data": {
    "id": "d1",
    "title": "TechCorp upgrade"
  }
}
Error
json
{
  "success": false,
  "error": "Validation failed",
  "code": "VALIDATION_ERROR",
  "details": [
    {
      "path": "email",
      "message": "Invalid email"
    }
  ]
}

Errors

The API uses conventional HTTP status codes. 2xx means success, 4xx means the request was rejected (problem with your input or auth state), and 5xx means something went wrong on our side and the call is safe to retry.

StatusCodeMeaning
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
409CONFLICTUnique constraint violation (e.g. duplicate email on contact create).
429RATE_LIMITEDToo many requests within the rate-limit window. Retry after the seconds reported in the error message.
500INTERNAL_ERRORUnexpected server failure. Safe to retry with exponential back-off.

Rate limiting

Auth endpoints are throttled per client IP. Authenticated endpoints inherit a default per-user budget. When you exceed the budget the response is 429 RATE_LIMITED with a human-readable retry hint in error.

EndpointLimitWindowScope
POST /auth/login10 req60 sper IP
POST /auth/refresh30 req60 sper IP

Pagination

List endpoints use opaque cursor pagination. Pass the nextCursor from the previous response back as cursor in the next call. When nextCursor is null, there are no more results. Default page size is 25; max is 100.

bash
# First page
curl 'https://app.upstreamcrm.com/api/v1/deals?limit=25' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

# Subsequent pages
curl 'https://app.upstreamcrm.com/api/v1/deals?limit=25&cursor=<lastDealId>' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Authentication

Email + password login that issues a short-lived JWT access token (15 min) and a rotating refresh token (30 days). Refresh tokens are single-use — reuse triggers automatic revocation of every active session for that user.

POST/auth/login
Public

Exchange email + password for an access/refresh token pair

Rate-limited to 10 requests/minute per IP. On success, also persists a hashed copy of the refresh token to `mobile_refresh_tokens` so it can be revoked server-side.

Request body

NameTypeDescription
emailrequiredstring (email)Lower-cased automatically.
passwordrequiredstring (1–200)Plain password — bcrypt-compared on the server.
device_infostring ≤ 500Optional device label. Defaults to User-Agent.

Request example

json
{
  "email": "jane@acme.com",
  "password": "correct horse battery staple",
  "device_info": "iPhone 15 Pro — iOS 18.2"
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"jane@acme.com","password":"correct horse battery staple","device_info":"iPhone 15 Pro — iOS 18.2"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOi...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOi...",
    "user": {
      "id": "4f5f6...",
      "email": "jane@acme.com",
      "fullName": "Jane Doe",
      "avatarUrl": null
    },
    "organization": {
      "id": "9a1b...",
      "name": "Acme Sales",
      "role": "admin"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDEmail not found or password mismatch (generic message — does not leak which).
400NO_ORGANIZATIONUser exists but has no organization memberships.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
429RATE_LIMITEDToo many requests within the rate-limit window. Retry after the seconds reported in the error message.
POST/auth/refresh
Public

Rotate a refresh token; receive a new access/refresh pair

Rate-limited to 30 requests/minute per IP. The presented refresh token is single-use — a successful call marks it as `revoked` and links it to the new token. If a previously-rotated token is presented again, every active session for that user is revoked (reuse-detection theft protection).

Request body

NameTypeDescription
refreshTokenrequiredstringThe refresh token returned by `/auth/login` or a previous `/auth/refresh`.

Request example

json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIi..."
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/auth/refresh' \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIi..."}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDToken invalid, expired, revoked, or reuse detected.
400NO_ORGANIZATIONUser no longer belongs to any organization.
429RATE_LIMITEDToo many requests within the rate-limit window. Retry after the seconds reported in the error message.
POST/auth/logout
Public

Revoke a refresh token

Idempotent. Always returns 200 — whether the token existed, was already revoked, or was malformed — to prevent enumeration.

Request body

NameTypeDescription
refreshTokenrequiredstringThe refresh token to revoke.

Request example

json
{
  "refreshToken": "eyJhbGciOi..."
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/auth/logout' \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"eyJhbGciOi..."}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "success": true
  }
}

Errors

StatusCodeWhen
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
GET/me
Bearer auth

Current user, current organization, and all memberships

Returns the authenticated user, the org currently encoded in the JWT, and every org the user belongs to (for an org switcher).

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/me' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "user": {
      "id": "4f5f6...",
      "email": "jane@acme.com",
      "fullName": "Jane Doe",
      "avatarUrl": null
    },
    "organization": {
      "id": "9a1b...",
      "name": "Acme Sales",
      "role": "admin"
    },
    "organizations": [
      {
        "id": "9a1b...",
        "name": "Acme Sales",
        "role": "admin"
      },
      {
        "id": "b22f...",
        "name": "Acme Labs",
        "role": "member"
      }
    ]
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.

Pipeline

Single endpoint that returns everything the mobile pipeline screen needs in one round-trip: stages with aggregate counts/values, and trimmed deal cards for the chosen pipeline.

GET/pipeline
Bearer auth

Pipeline + stages + deal cards for the mobile home screen

Defaults to the first active pipeline if `pipelineId` is omitted. Respects partner scoping (`canViewAllOrgData = false`) automatically.

Query parameters

NameTypeDescription
pipelineIduuidPipeline to fetch. Defaults to the first active one in the org.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/pipeline' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "pipeline": {
      "id": "a11c...",
      "name": "Default Pipeline"
    },
    "pipelines": [
      {
        "id": "a11c...",
        "name": "Default Pipeline"
      }
    ],
    "stages": [
      {
        "id": "s1",
        "name": "Qualified",
        "orderIndex": 1,
        "color": "#3b82f6",
        "probability": 20,
        "dealCount": 4,
        "totalValue": 45000
      },
      {
        "id": "s2",
        "name": "Demo",
        "orderIndex": 2,
        "color": "#a855f7",
        "probability": 50,
        "dealCount": 2,
        "totalValue": 32000
      }
    ],
    "deals": [
      {
        "id": "d1",
        "title": "TechCorp upgrade",
        "value": 12000,
        "currency": "USD",
        "probability": 30,
        "temperature": "hot",
        "stageId": "s1",
        "updatedAt": "2026-05-10T14:22:31.000Z",
        "expectedCloseDate": "2026-06-01T00:00:00.000Z",
        "contact": {
          "id": "c1",
          "name": "John Reed",
          "phone": "+1...",
          "email": "j@techcorp.com"
        },
        "company": {
          "id": "co1",
          "name": "TechCorp"
        },
        "owner": {
          "id": "u1",
          "name": "Jane Doe",
          "avatarUrl": null
        },
        "activityStatus": "upcoming",
        "daysSinceLastActivity": 2
      }
    ]
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.

Deals

Full CRUD plus dedicated transitions for stage moves and won/lost status changes. Soft-deletes via `is_deleted` — deletes are reversible from the web app.

GET/deals
Bearer auth

Cursor-paginated deal list with filters

Sort: most recently updated first. Cursor pagination via the last `id` of the previous page.

Query parameters

NameTypeDescription
pipelineIduuidRestrict to one pipeline.
stageIduuidRestrict to one stage.
status'open' | 'won' | 'lost'Status filter.
ownerIduuidRestrict to deals owned by a specific user.
searchstring (1–200)Case-insensitive title/company/contact match.
cursoruuidLast deal id from the previous page.
limitint 1–100Default 25.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/deals' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "d1",
        "title": "TechCorp upgrade",
        "value": 12000,
        "currency": "USD",
        "status": "open",
        "temperature": "hot",
        "probability": 30,
        "stageId": "s1",
        "stageName": "Qualified",
        "pipelineId": "a11c...",
        "expectedCloseDate": "2026-06-01T00:00:00.000Z",
        "updatedAt": "2026-05-10T14:22:31.000Z",
        "contact": {
          "id": "c1",
          "name": "John Reed",
          "phone": "+1...",
          "email": "j@techcorp.com"
        },
        "company": {
          "id": "co1",
          "name": "TechCorp"
        },
        "owner": {
          "id": "u1",
          "name": "Jane Doe",
          "avatarUrl": null
        }
      }
    ],
    "nextCursor": "d1"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
POST/deals
Bearer auth

Create a deal

Required: `title` and `stageId`. If `pipelineId` is omitted, it is inferred from the stage. `templateId` optionally applies an activity template on create.

Request body

NameTypeDescription
titlerequiredstring (1–255)Deal title.
stageIdrequireduuidTarget pipeline stage.
valuenumber ≥ 0Deal value in `currency`.
currencystring (1–10)ISO currency code, e.g. `USD`, `INR`. Defaults to org currency.
pipelineIduuidInferred from stage if omitted.
contactIduuid | nullPrimary contact.
companyIduuid | nullRelated company.
sourceIduuid | nullLead source.
partnerIduuid | nullReferring partner.
probabilityint 0–100 | nullDefaults to stage probability.
expectedCloseDateISO 8601 | nullTarget close date.
visibility'private' | 'team' | 'organization'Default `organization`.
notesstring ≤ 10000 | nullFree-text notes.
temperature'hot' | 'cold'Defaults to `cold`.
templateIduuid | nullApply an activity template after create.

Request example

json
{
  "title": "TechCorp upgrade",
  "value": 12000,
  "currency": "USD",
  "stageId": "s1",
  "contactId": "c1",
  "companyId": "co1",
  "probability": 30,
  "expectedCloseDate": "2026-06-01T00:00:00.000Z",
  "temperature": "hot"
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/deals' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"title":"TechCorp upgrade","value":12000,"currency":"USD","stageId":"s1","contactId":"c1","companyId":"co1","probability":30,"expectedCloseDate":"2026-06-01T00:00:00.000Z","temperature":"hot"}'

Response 201 OK

json
{
  "success": true,
  "data": {
    "id": "d1",
    "title": "TechCorp upgrade",
    "value": 12000,
    "currency": "USD",
    "status": "open",
    "temperature": "hot",
    "probability": 30,
    "stageId": "s1",
    "stageName": "Qualified",
    "pipelineId": "a11c..."
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
GET/deals/{id}
Bearer auth

Fetch a single deal with detail fields

Returns the same shape as `deals-list` items plus `notes`, `lostReason`, `visibility`, `createdAt`, `source`, `partner`, and `recentActivityCount`.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/deals/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "d1",
    "title": "TechCorp upgrade",
    "value": 12000,
    "currency": "USD",
    "status": "open",
    "temperature": "hot",
    "probability": 30,
    "stageId": "s1",
    "stageName": "Qualified",
    "pipelineId": "a11c...",
    "notes": "Spoke with John — needs procurement sign-off.",
    "lostReason": null,
    "visibility": "organization",
    "createdAt": "2026-04-12T09:01:00.000Z",
    "source": {
      "id": "src1",
      "name": "Inbound — website"
    },
    "partner": null,
    "recentActivityCount": 4
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
PATCH/deals/{id}
Bearer auth

Partially update a deal

All fields optional. Set a value to `null` to clear it. `status` is **not** mutable here — use `/deals/{id}/status`. `stageId` is **not** mutable here — use `/deals/{id}/stage`.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Request body

NameTypeDescription
titlestring (1–255)
valuenumber ≥ 0
currencystring
contactIduuid | null
companyIduuid | null
sourceIduuid | null
partnerIduuid | null
probabilityint 0–100 | null
expectedCloseDateISO 8601 | null
visibility'private' | 'team' | 'organization'
notesstring ≤ 10000 | null
temperature'hot' | 'cold'

Request example

json
{
  "value": 15000,
  "probability": 50,
  "temperature": "hot"
}

Example request — cURL

bash
curl -X PATCH 'https://app.upstreamcrm.com/api/v1/deals/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"value":15000,"probability":50,"temperature":"hot"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "d1",
    "value": 15000,
    "probability": 50,
    "temperature": "hot"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
DELETE/deals/{id}
Bearer auth

Soft-delete a deal

Sets `is_deleted = true`. The deal can be restored from the web app `Deals → Deleted` view.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Example request — cURL

bash
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/deals/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "deleted": true
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
PATCH/deals/{id}/stage
Bearer auth

Move a deal to another stage

Records a row in `deal_stage_history` and logs a `deal_stage_changed` activity. Will refuse to move into a Won/Lost protected stage — use `/deals/{id}/status` instead.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Request body

NameTypeDescription
stageIdrequireduuidDestination stage (must belong to the same pipeline).

Request example

json
{
  "stageId": "s2"
}

Example request — cURL

bash
curl -X PATCH 'https://app.upstreamcrm.com/api/v1/deals/{id}/stage' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"stageId":"s2"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "d1",
    "stageId": "s2",
    "stageName": "Demo"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
PATCH/deals/{id}/status
Bearer auth

Mark a deal as won, lost, or re-open it

`lostReason` is required when `status = lost`. Pass `cancelRemainingOnLost = true` to auto-cancel open activity-template steps on loss.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Request body

NameTypeDescription
statusrequired'open' | 'won' | 'lost'New status.
lostReasonstring ≤ 500Required when `status = lost`.
cancelRemainingOnLostbooleanCancel open template steps if losing.

Request example

json
{
  "status": "won"
}

Example request — cURL

bash
curl -X PATCH 'https://app.upstreamcrm.com/api/v1/deals/{id}/status' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"status":"won"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "d1",
    "status": "won",
    "stageId": "won-stage-id"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.

Contacts

CRUD over the org's contact directory. Soft-deletes; restore from the web app.

GET/contacts
Bearer auth

Cursor-paginated contact list

Free-text search matches first name, last name, email, and phone.

Query parameters

NameTypeDescription
searchstring (1–200)Free-text search.
companyIduuidFilter by company.
ownerIduuidFilter by owner.
cursoruuidLast contact id from prior page.
limitint 1–100Default 25.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/contacts' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "c1",
        "firstName": "John",
        "lastName": "Reed",
        "email": "j@techcorp.com",
        "phone": "+1...",
        "jobTitle": "VP Eng",
        "company": {
          "id": "co1",
          "name": "TechCorp"
        }
      }
    ],
    "nextCursor": "c1"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
POST/contacts
Bearer auth

Create a contact

Only `firstName` is required.

Request body

NameTypeDescription
firstNamerequiredstring (1–100)
lastNamestring ≤ 100
emailemail | null
phonestring ≤ 50 | nullStored as-entered.
jobTitlestring ≤ 100 | null
companyIduuid | null
addressstring ≤ 500 | null
citystring ≤ 100 | null
statestring ≤ 100 | null
countrystring ≤ 100 | null
postalCodestring ≤ 20 | null
notesstring ≤ 10000 | null

Request example

json
{
  "firstName": "John",
  "lastName": "Reed",
  "email": "j@techcorp.com",
  "companyId": "co1",
  "jobTitle": "VP Eng"
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/contacts' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"firstName":"John","lastName":"Reed","email":"j@techcorp.com","companyId":"co1","jobTitle":"VP Eng"}'

Response 201 OK

json
{
  "success": true,
  "data": {
    "id": "c1",
    "firstName": "John",
    "lastName": "Reed",
    "email": "j@techcorp.com",
    "company": {
      "id": "co1",
      "name": "TechCorp"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
409CONFLICTUnique constraint violation (e.g. duplicate email on contact create).
GET/contacts/{id}
Bearer auth

Fetch one contact

Path parameters

NameTypeDescription
idrequireduuidContact id.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/contacts/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "c1",
    "firstName": "John",
    "lastName": "Reed",
    "email": "j@techcorp.com",
    "phone": "+1...",
    "jobTitle": "VP Eng",
    "company": {
      "id": "co1",
      "name": "TechCorp"
    },
    "address": "...",
    "city": "San Francisco",
    "country": "USA",
    "notes": null
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
PATCH/contacts/{id}
Bearer auth

Partially update a contact

Path parameters

NameTypeDescription
idrequireduuidContact id.

Request body

NameTypeDescription
firstNamestring (1–100)
lastNamestring ≤ 100
emailemail | null
phonestring | null
jobTitlestring | null
companyIduuid | null
addressstring | null
citystring | null
statestring | null
countrystring | null
postalCodestring | null
notesstring | null

Request example

json
{
  "jobTitle": "CTO",
  "phone": "+1 415 555 0144"
}

Example request — cURL

bash
curl -X PATCH 'https://app.upstreamcrm.com/api/v1/contacts/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"jobTitle":"CTO","phone":"+1 415 555 0144"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "c1",
    "jobTitle": "CTO",
    "phone": "+1 415 555 0144"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
DELETE/contacts/{id}
Bearer auth

Soft-delete a contact

Path parameters

NameTypeDescription
idrequireduuidContact id.

Example request — cURL

bash
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/contacts/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "deleted": true
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.

Activities

Calls, emails, meetings, messages, tasks, notes, demos and physical visits. Includes dedicated `complete` / `uncomplete` transitions so progress through an activity template advances automatically.

GET/activities
Bearer auth

Cursor-paginated activity list with rich filters

`status` filters by computed activity status (overdue, due today, upcoming, completed, open). Date filters operate on `due_date`.

Query parameters

NameTypeDescription
type'call'|'email'|'meeting'|'message'|'task'|'note'|'demo'|'physical_visit'Activity type.
status'open' | 'completed' | 'overdue' | 'due_today' | 'upcoming'Computed status.
assigneeIduuidFilter by assignee.
dealIduuidActivities for a single deal.
contactIduuidActivities for a single contact.
dateFromISO 8601Inclusive lower bound on due date.
dateToISO 8601Inclusive upper bound on due date.
cursoruuidLast activity id from prior page.
limitint 1–100Default 25.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/activities' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "a1",
        "type": "call",
        "title": "Follow-up call with John",
        "description": null,
        "dueDate": "2026-05-13T15:00:00.000Z",
        "completedAt": null,
        "durationMinutes": 30,
        "status": "upcoming",
        "assignee": {
          "id": "u1",
          "name": "Jane Doe",
          "avatarUrl": null
        },
        "deal": {
          "id": "d1",
          "title": "TechCorp upgrade"
        },
        "contact": {
          "id": "c1",
          "name": "John Reed"
        }
      }
    ],
    "nextCursor": "a1"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
POST/activities
Bearer auth

Create an activity

Pass `completedAt` to create an activity that's already completed (e.g. logging a call after the fact).

Request body

NameTypeDescription
typerequiredenumSee enum on `GET /activities`.
titlerequiredstring (1–255)
descriptionstring ≤ 10000 | null
dueDateISO 8601 | null
assigneeIduuid | nullDefaults to caller.
dealIduuid | null
contactIduuid | null
durationMinutesint > 0 | nullFor calls / meetings.
completedAtISO 8601 | nullSet to log a past activity as already done.

Request example

json
{
  "type": "call",
  "title": "Follow-up call with John",
  "dealId": "d1",
  "contactId": "c1",
  "durationMinutes": 30,
  "dueDate": "2026-05-13T15:00:00.000Z"
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/activities' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"type":"call","title":"Follow-up call with John","dealId":"d1","contactId":"c1","durationMinutes":30,"dueDate":"2026-05-13T15:00:00.000Z"}'

Response 201 OK

json
{
  "success": true,
  "data": {
    "id": "a1",
    "type": "call",
    "title": "Follow-up call with John",
    "status": "upcoming"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
PATCH/activities/{id}
Bearer auth

Partially update an activity

Cannot toggle `completed` here — use the dedicated complete / uncomplete endpoints so template progress stays consistent.

Path parameters

NameTypeDescription
idrequireduuidActivity id.

Request body

NameTypeDescription
typeenum
titlestring (1–255)
descriptionstring | null
dueDateISO 8601 | null
assigneeIduuid | null
dealIduuid | null
contactIduuid | null
durationMinutesint > 0 | null

Request example

json
{
  "dueDate": "2026-05-14T15:00:00.000Z",
  "durationMinutes": 45
}

Example request — cURL

bash
curl -X PATCH 'https://app.upstreamcrm.com/api/v1/activities/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"dueDate":"2026-05-14T15:00:00.000Z","durationMinutes":45}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "a1",
    "dueDate": "2026-05-14T15:00:00.000Z",
    "durationMinutes": 45
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
DELETE/activities/{id}
Bearer auth

Delete an activity

If the activity was part of a deal template, the response includes the next pending step (if any) so the UI can update.

Path parameters

NameTypeDescription
idrequireduuidActivity id.

Example request — cURL

bash
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/activities/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "deleted": true,
    "nextPendingStep": {
      "stepIndex": 2,
      "type": "email",
      "title": "Send proposal"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
POST/activities/{id}/complete
Bearer auth

Mark an activity as complete

Sets `completed_at`. If the activity is a template step, advances the deal's template progress.

Path parameters

NameTypeDescription
idrequireduuidActivity id.

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/activities/{id}/complete' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "a1",
    "completedAt": "2026-05-12T10:31:00.000Z",
    "nextPendingStep": {
      "stepIndex": 2,
      "type": "email",
      "title": "Send proposal"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
POST/activities/{id}/uncomplete
Bearer auth

Re-open a completed activity

Clears `completed_at`. Rolls back template progress if applicable.

Path parameters

NameTypeDescription
idrequireduuidActivity id.

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/activities/{id}/uncomplete' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "a1",
    "completedAt": null
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.

Activity Templates

Reusable sequences of activity steps (e.g. "Enterprise discovery: call → demo → proposal → follow-up") that can be applied to a deal.

GET/activity-templates
Bearer auth

List all activity templates in the organization

Query parameters

NameTypeDescription
includeArchivedbooleanInclude archived templates. Default `false`.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/activity-templates' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": [
    {
      "id": "t1",
      "name": "Enterprise discovery",
      "description": "Standard 4-step discovery sequence",
      "steps": [
        {
          "id": "st1",
          "orderIndex": 1,
          "type": "call",
          "title": "Discovery call"
        },
        {
          "id": "st2",
          "orderIndex": 2,
          "type": "demo",
          "title": "Product demo"
        }
      ],
      "archivedAt": null
    }
  ]
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
POST/activity-templates
Bearer auth

Create an activity template

Each step requires `type` and `title`.

Request body

NameTypeDescription
namerequiredstring (1–255)
descriptionstring ≤ 2000 | null
steps[].typerequiredenumActivity type.
steps[].titlerequiredstring (1–255)Step title.
steps[].descriptionstring ≤ 2000 | null

Request example

json
{
  "name": "Enterprise discovery",
  "description": "Standard 4-step discovery",
  "steps": [
    {
      "type": "call",
      "title": "Discovery call"
    },
    {
      "type": "demo",
      "title": "Product demo"
    },
    {
      "type": "email",
      "title": "Send proposal"
    },
    {
      "type": "task",
      "title": "Internal review"
    }
  ]
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/activity-templates' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Enterprise discovery","description":"Standard 4-step discovery","steps":[{"type":"call","title":"Discovery call"},{"type":"demo","title":"Product demo"},{"type":"email","title":"Send proposal"},{"type":"task","title":"Internal review"}]}'

Response 201 OK

json
{
  "success": true,
  "data": {
    "id": "t1",
    "name": "Enterprise discovery"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
GET/activity-templates/{id}
Bearer auth

Fetch one activity template

Path parameters

NameTypeDescription
idrequireduuidTemplate id.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/activity-templates/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "t1",
    "name": "Enterprise discovery",
    "steps": []
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
PATCH/activity-templates/{id}
Bearer auth

Update a template (incl. replace steps)

If `steps` is provided, the template's step list is fully replaced. Existing step `id`s present in the body are preserved; missing ones are deleted; new entries are inserted.

Path parameters

NameTypeDescription
idrequireduuidTemplate id.

Request body

NameTypeDescription
namestring (1–255)
descriptionstring | null
stepsarrayFull step list. Existing step ids preserved; omitted ones deleted.

Request example

json
{
  "name": "Enterprise discovery v2",
  "steps": [
    {
      "id": "st1",
      "type": "call",
      "title": "Discovery call"
    },
    {
      "type": "meeting",
      "title": "Solution workshop"
    }
  ]
}

Example request — cURL

bash
curl -X PATCH 'https://app.upstreamcrm.com/api/v1/activity-templates/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Enterprise discovery v2","steps":[{"id":"st1","type":"call","title":"Discovery call"},{"type":"meeting","title":"Solution workshop"}]}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "t1",
    "name": "Enterprise discovery v2"
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
DELETE/activity-templates/{id}
Bearer auth

Archive a template

Soft-archive; existing deals already on this template are unaffected.

Path parameters

NameTypeDescription
idrequireduuidTemplate id.

Example request — cURL

bash
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/activity-templates/{id}' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "deleted": true
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.

Devices

Register and revoke Expo push tokens for the authenticated user. The mobile app calls register after sign-in and on every launch; it calls revoke during sign-out.

POST/devices/register
Bearer auth

Register / refresh an Expo push token for this user

Idempotent upsert keyed on `expoPushToken`. Re-registering the same token on every app launch is cheap and recommended — it refreshes `last_seen_at` and re-activates a previously-revoked row.

Request body

NameTypeDescription
expoPushTokenrequiredstring (10–255)The Expo push token returned by `Notifications.getExpoPushTokenAsync()`. Must start with `ExponentPushToken[` or `ExpoPushToken[`.
platformrequired'ios' | 'android'Device platform.
deviceInfostring ≤ 500Optional device label. Defaults to User-Agent.

Request example

json
{
  "expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
  "platform": "ios",
  "deviceInfo": "iPhone 15 Pro — iOS 18.2"
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/devices/register' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"expoPushToken":"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]","platform":"ios","deviceInfo":"iPhone 15 Pro — iOS 18.2"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "id": "7c1a...",
    "expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
    "platform": "ios",
    "createdAt": "2026-05-12T10:00:00.000Z"
  }
}

Errors

StatusCodeWhen
400INVALID_PUSH_TOKENThe supplied token does not match the Expo push token format.
401UNAUTHORIZEDMissing, invalid or expired access token.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
DELETE/devices/register
Bearer auth

Revoke a push token (called on sign-out)

Idempotent. Returns 200 whether the token existed or not.

Request body

NameTypeDescription
expoPushTokenrequiredstring (10–255)The token to revoke. Must belong to the calling user.

Request example

json
{
  "expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
}

Example request — cURL

bash
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/devices/register' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"expoPushToken":"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "revoked": true
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.

Deal ↔ Template

Apply, inspect or detach an activity template on a specific deal, and schedule the next step.

GET/deals/{id}/template
Bearer auth

Current template progress on a deal

Returns the template (if any) and the status of each step.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Example request — cURL

bash
curl -X GET 'https://app.upstreamcrm.com/api/v1/deals/{id}/template' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Response 200 OK

json
{
  "success": true,
  "data": {
    "template": {
      "id": "t1",
      "name": "Enterprise discovery"
    },
    "steps": [
      {
        "stepIndex": 1,
        "type": "call",
        "title": "Discovery call",
        "status": "completed",
        "activityId": "a1"
      },
      {
        "stepIndex": 2,
        "type": "demo",
        "title": "Product demo",
        "status": "pending",
        "activityId": null
      }
    ],
    "nextPendingStep": {
      "stepIndex": 2,
      "type": "demo",
      "title": "Product demo"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
PUT/deals/{id}/template
Bearer auth

Attach or detach a template from a deal

Pass `templateId: null` to detach the current template.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Request body

NameTypeDescription
templateIdrequireduuid | nullTemplate to apply, or `null` to detach.

Request example

json
{
  "templateId": "t1"
}

Example request — cURL

bash
curl -X PUT 'https://app.upstreamcrm.com/api/v1/deals/{id}/template' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"templateId":"t1"}'

Response 200 OK

json
{
  "success": true,
  "data": {
    "templateId": "t1",
    "nextPendingStep": {
      "stepIndex": 1,
      "type": "call",
      "title": "Discovery call"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.
POST/deals/{id}/template/schedule-next
Bearer auth

Schedule the next pending template step as an activity

Creates the activity for the deal's next pending step, using the supplied `dueDate`.

Path parameters

NameTypeDescription
idrequireduuidDeal id.

Request body

NameTypeDescription
dueDaterequiredISO 8601When the next step should be due.
notesstring ≤ 10000 | nullOptional notes on the new activity.

Request example

json
{
  "dueDate": "2026-05-15T14:00:00.000Z"
}

Example request — cURL

bash
curl -X POST 'https://app.upstreamcrm.com/api/v1/deals/{id}/template/schedule-next' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"dueDate":"2026-05-15T14:00:00.000Z"}'

Response 201 OK

json
{
  "success": true,
  "data": {
    "activity": {
      "id": "a2",
      "type": "demo",
      "title": "Product demo",
      "dueDate": "2026-05-15T14:00:00.000Z"
    },
    "nextPendingStep": {
      "stepIndex": 3,
      "type": "email",
      "title": "Send proposal"
    }
  }
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing, invalid or expired access token.
403FORBIDDENToken is valid but the user is not a member of the target organization.
404NOT_FOUNDThe requested resource does not exist or is not visible to the caller.
400VALIDATION_ERRORRequest body or query failed Zod schema validation. `details[]` lists each offending field.