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.
15-minute access tokens, rotating refresh tokens with reuse detection.
Every request is scoped to the organization in your token — never trust the body.
Per-IP and per-user limits on sensitive routes (login, refresh).
All endpoints live under /api/v1. Breaking changes go in /api/v2.
https://app.upstreamcrm.com/api/v1Machine-readable contract. Import into Postman, Insomnia, or generate clients with openapi-generator.
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.
GET /api/v1/me HTTP/1.1
Host: app.upstreamcrm.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIi...
Accept: application/jsonAsyncStorage 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": true,
"data": {
"id": "d1",
"title": "TechCorp upgrade"
}
}{
"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.
| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
| 409 | CONFLICT | Unique constraint violation (e.g. duplicate email on contact create). |
| 429 | RATE_LIMITED | Too many requests within the rate-limit window. Retry after the seconds reported in the error message. |
| 500 | INTERNAL_ERROR | Unexpected 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.
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
| POST /auth/login | 10 req | 60 s | per IP |
| POST /auth/refresh | 30 req | 60 s | per 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.
# 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.
/auth/loginExchange 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
| Name | Type | Description |
|---|---|---|
| emailrequired | string (email) | Lower-cased automatically. |
| passwordrequired | string (1–200) | Plain password — bcrypt-compared on the server. |
| device_info | string ≤ 500 | Optional device label. Defaults to User-Agent. |
Request example
{
"email": "jane@acme.com",
"password": "correct horse battery staple",
"device_info": "iPhone 15 Pro — iOS 18.2"
}Example request — cURL
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
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Email not found or password mismatch (generic message — does not leak which). |
| 400 | NO_ORGANIZATION | User exists but has no organization memberships. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
| 429 | RATE_LIMITED | Too many requests within the rate-limit window. Retry after the seconds reported in the error message. |
/auth/refreshRotate 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
| Name | Type | Description |
|---|---|---|
| refreshTokenrequired | string | The refresh token returned by `/auth/login` or a previous `/auth/refresh`. |
Request example
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIi..."
}Example request — cURL
curl -X POST 'https://app.upstreamcrm.com/api/v1/auth/refresh' \
-H 'Content-Type: application/json' \
-d '{"refreshToken":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIi..."}'Response 200 OK
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Token invalid, expired, revoked, or reuse detected. |
| 400 | NO_ORGANIZATION | User no longer belongs to any organization. |
| 429 | RATE_LIMITED | Too many requests within the rate-limit window. Retry after the seconds reported in the error message. |
/auth/logoutRevoke a refresh token
Idempotent. Always returns 200 — whether the token existed, was already revoked, or was malformed — to prevent enumeration.
Request body
| Name | Type | Description |
|---|---|---|
| refreshTokenrequired | string | The refresh token to revoke. |
Request example
{
"refreshToken": "eyJhbGciOi..."
}Example request — cURL
curl -X POST 'https://app.upstreamcrm.com/api/v1/auth/logout' \
-H 'Content-Type: application/json' \
-d '{"refreshToken":"eyJhbGciOi..."}'Response 200 OK
{
"success": true,
"data": {
"success": true
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/meCurrent 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
curl -X GET 'https://app.upstreamcrm.com/api/v1/me' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token 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.
/pipelinePipeline + 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
| Name | Type | Description |
|---|---|---|
| pipelineId | uuid | Pipeline to fetch. Defaults to the first active one in the org. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/pipeline' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 400 | VALIDATION_ERROR | Request 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.
/dealsCursor-paginated deal list with filters
Sort: most recently updated first. Cursor pagination via the last `id` of the previous page.
Query parameters
| Name | Type | Description |
|---|---|---|
| pipelineId | uuid | Restrict to one pipeline. |
| stageId | uuid | Restrict to one stage. |
| status | 'open' | 'won' | 'lost' | Status filter. |
| ownerId | uuid | Restrict to deals owned by a specific user. |
| search | string (1–200) | Case-insensitive title/company/contact match. |
| cursor | uuid | Last deal id from the previous page. |
| limit | int 1–100 | Default 25. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/deals' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/dealsCreate 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
| Name | Type | Description |
|---|---|---|
| titlerequired | string (1–255) | Deal title. |
| stageIdrequired | uuid | Target pipeline stage. |
| value | number ≥ 0 | Deal value in `currency`. |
| currency | string (1–10) | ISO currency code, e.g. `USD`, `INR`. Defaults to org currency. |
| pipelineId | uuid | Inferred from stage if omitted. |
| contactId | uuid | null | Primary contact. |
| companyId | uuid | null | Related company. |
| sourceId | uuid | null | Lead source. |
| partnerId | uuid | null | Referring partner. |
| probability | int 0–100 | null | Defaults to stage probability. |
| expectedCloseDate | ISO 8601 | null | Target close date. |
| visibility | 'private' | 'team' | 'organization' | Default `organization`. |
| notes | string ≤ 10000 | null | Free-text notes. |
| temperature | 'hot' | 'cold' | Defaults to `cold`. |
| templateId | uuid | null | Apply an activity template after create. |
Request example
{
"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
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
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/deals/{id}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
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/deals/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/deals/{id}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
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Request body
| Name | Type | Description |
|---|---|---|
| title | string (1–255) | |
| value | number ≥ 0 | |
| currency | string | |
| contactId | uuid | null | |
| companyId | uuid | null | |
| sourceId | uuid | null | |
| partnerId | uuid | null | |
| probability | int 0–100 | null | |
| expectedCloseDate | ISO 8601 | null | |
| visibility | 'private' | 'team' | 'organization' | |
| notes | string ≤ 10000 | null | |
| temperature | 'hot' | 'cold' |
Request example
{
"value": 15000,
"probability": 50,
"temperature": "hot"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "d1",
"value": 15000,
"probability": 50,
"temperature": "hot"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/deals/{id}Soft-delete a deal
Sets `is_deleted = true`. The deal can be restored from the web app `Deals → Deleted` view.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Example request — cURL
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/deals/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"deleted": true
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/deals/{id}/stageMove 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
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Request body
| Name | Type | Description |
|---|---|---|
| stageIdrequired | uuid | Destination stage (must belong to the same pipeline). |
Request example
{
"stageId": "s2"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "d1",
"stageId": "s2",
"stageName": "Demo"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/deals/{id}/statusMark 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
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Request body
| Name | Type | Description |
|---|---|---|
| statusrequired | 'open' | 'won' | 'lost' | New status. |
| lostReason | string ≤ 500 | Required when `status = lost`. |
| cancelRemainingOnLost | boolean | Cancel open template steps if losing. |
Request example
{
"status": "won"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "d1",
"status": "won",
"stageId": "won-stage-id"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request 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.
/contactsCursor-paginated contact list
Free-text search matches first name, last name, email, and phone.
Query parameters
| Name | Type | Description |
|---|---|---|
| search | string (1–200) | Free-text search. |
| companyId | uuid | Filter by company. |
| ownerId | uuid | Filter by owner. |
| cursor | uuid | Last contact id from prior page. |
| limit | int 1–100 | Default 25. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/contacts' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/contactsCreate a contact
Only `firstName` is required.
Request body
| Name | Type | Description |
|---|---|---|
| firstNamerequired | string (1–100) | |
| lastName | string ≤ 100 | |
| email | null | ||
| phone | string ≤ 50 | null | Stored as-entered. |
| jobTitle | string ≤ 100 | null | |
| companyId | uuid | null | |
| address | string ≤ 500 | null | |
| city | string ≤ 100 | null | |
| state | string ≤ 100 | null | |
| country | string ≤ 100 | null | |
| postalCode | string ≤ 20 | null | |
| notes | string ≤ 10000 | null |
Request example
{
"firstName": "John",
"lastName": "Reed",
"email": "j@techcorp.com",
"companyId": "co1",
"jobTitle": "VP Eng"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "c1",
"firstName": "John",
"lastName": "Reed",
"email": "j@techcorp.com",
"company": {
"id": "co1",
"name": "TechCorp"
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
| 409 | CONFLICT | Unique constraint violation (e.g. duplicate email on contact create). |
/contacts/{id}Fetch one contact
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Contact id. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/contacts/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/contacts/{id}Partially update a contact
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Contact id. |
Request body
| Name | Type | Description |
|---|---|---|
| firstName | string (1–100) | |
| lastName | string ≤ 100 | |
| email | null | ||
| phone | string | null | |
| jobTitle | string | null | |
| companyId | uuid | null | |
| address | string | null | |
| city | string | null | |
| state | string | null | |
| country | string | null | |
| postalCode | string | null | |
| notes | string | null |
Request example
{
"jobTitle": "CTO",
"phone": "+1 415 555 0144"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "c1",
"jobTitle": "CTO",
"phone": "+1 415 555 0144"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/contacts/{id}Soft-delete a contact
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Contact id. |
Example request — cURL
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/contacts/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"deleted": true
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The 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.
/activitiesCursor-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
| Name | Type | Description |
|---|---|---|
| type | 'call'|'email'|'meeting'|'message'|'task'|'note'|'demo'|'physical_visit' | Activity type. |
| status | 'open' | 'completed' | 'overdue' | 'due_today' | 'upcoming' | Computed status. |
| assigneeId | uuid | Filter by assignee. |
| dealId | uuid | Activities for a single deal. |
| contactId | uuid | Activities for a single contact. |
| dateFrom | ISO 8601 | Inclusive lower bound on due date. |
| dateTo | ISO 8601 | Inclusive upper bound on due date. |
| cursor | uuid | Last activity id from prior page. |
| limit | int 1–100 | Default 25. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/activities' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/activitiesCreate an activity
Pass `completedAt` to create an activity that's already completed (e.g. logging a call after the fact).
Request body
| Name | Type | Description |
|---|---|---|
| typerequired | enum | See enum on `GET /activities`. |
| titlerequired | string (1–255) | |
| description | string ≤ 10000 | null | |
| dueDate | ISO 8601 | null | |
| assigneeId | uuid | null | Defaults to caller. |
| dealId | uuid | null | |
| contactId | uuid | null | |
| durationMinutes | int > 0 | null | For calls / meetings. |
| completedAt | ISO 8601 | null | Set to log a past activity as already done. |
Request example
{
"type": "call",
"title": "Follow-up call with John",
"dealId": "d1",
"contactId": "c1",
"durationMinutes": 30,
"dueDate": "2026-05-13T15:00:00.000Z"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "a1",
"type": "call",
"title": "Follow-up call with John",
"status": "upcoming"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/activities/{id}Partially update an activity
Cannot toggle `completed` here — use the dedicated complete / uncomplete endpoints so template progress stays consistent.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Activity id. |
Request body
| Name | Type | Description |
|---|---|---|
| type | enum | |
| title | string (1–255) | |
| description | string | null | |
| dueDate | ISO 8601 | null | |
| assigneeId | uuid | null | |
| dealId | uuid | null | |
| contactId | uuid | null | |
| durationMinutes | int > 0 | null |
Request example
{
"dueDate": "2026-05-14T15:00:00.000Z",
"durationMinutes": 45
}Example request — cURL
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
{
"success": true,
"data": {
"id": "a1",
"dueDate": "2026-05-14T15:00:00.000Z",
"durationMinutes": 45
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/activities/{id}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
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Activity id. |
Example request — cURL
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/activities/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"deleted": true,
"nextPendingStep": {
"stepIndex": 2,
"type": "email",
"title": "Send proposal"
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/activities/{id}/completeMark an activity as complete
Sets `completed_at`. If the activity is a template step, advances the deal's template progress.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Activity id. |
Example request — cURL
curl -X POST 'https://app.upstreamcrm.com/api/v1/activities/{id}/complete' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"id": "a1",
"completedAt": "2026-05-12T10:31:00.000Z",
"nextPendingStep": {
"stepIndex": 2,
"type": "email",
"title": "Send proposal"
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/activities/{id}/uncompleteRe-open a completed activity
Clears `completed_at`. Rolls back template progress if applicable.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Activity id. |
Example request — cURL
curl -X POST 'https://app.upstreamcrm.com/api/v1/activities/{id}/uncomplete' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"id": "a1",
"completedAt": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The 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.
/activity-templatesList all activity templates in the organization
Query parameters
| Name | Type | Description |
|---|---|---|
| includeArchived | boolean | Include archived templates. Default `false`. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/activity-templates' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
/activity-templatesCreate an activity template
Each step requires `type` and `title`.
Request body
| Name | Type | Description |
|---|---|---|
| namerequired | string (1–255) | |
| description | string ≤ 2000 | null | |
| steps[].typerequired | enum | Activity type. |
| steps[].titlerequired | string (1–255) | Step title. |
| steps[].description | string ≤ 2000 | null |
Request example
{
"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
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
{
"success": true,
"data": {
"id": "t1",
"name": "Enterprise discovery"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/activity-templates/{id}Fetch one activity template
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Template id. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/activity-templates/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"id": "t1",
"name": "Enterprise discovery",
"steps": []
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/activity-templates/{id}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
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Template id. |
Request body
| Name | Type | Description |
|---|---|---|
| name | string (1–255) | |
| description | string | null | |
| steps | array | Full step list. Existing step ids preserved; omitted ones deleted. |
Request example
{
"name": "Enterprise discovery v2",
"steps": [
{
"id": "st1",
"type": "call",
"title": "Discovery call"
},
{
"type": "meeting",
"title": "Solution workshop"
}
]
}Example request — cURL
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
{
"success": true,
"data": {
"id": "t1",
"name": "Enterprise discovery v2"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/activity-templates/{id}Archive a template
Soft-archive; existing deals already on this template are unaffected.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Template id. |
Example request — cURL
curl -X DELETE 'https://app.upstreamcrm.com/api/v1/activity-templates/{id}' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"success": true,
"data": {
"deleted": true
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The 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.
/devices/registerRegister / 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
| Name | Type | Description |
|---|---|---|
| expoPushTokenrequired | string (10–255) | The Expo push token returned by `Notifications.getExpoPushTokenAsync()`. Must start with `ExponentPushToken[` or `ExpoPushToken[`. |
| platformrequired | 'ios' | 'android' | Device platform. |
| deviceInfo | string ≤ 500 | Optional device label. Defaults to User-Agent. |
Request example
{
"expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"platform": "ios",
"deviceInfo": "iPhone 15 Pro — iOS 18.2"
}Example request — cURL
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
{
"success": true,
"data": {
"id": "7c1a...",
"expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"platform": "ios",
"createdAt": "2026-05-12T10:00:00.000Z"
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_PUSH_TOKEN | The supplied token does not match the Expo push token format. |
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/devices/registerRevoke a push token (called on sign-out)
Idempotent. Returns 200 whether the token existed or not.
Request body
| Name | Type | Description |
|---|---|---|
| expoPushTokenrequired | string (10–255) | The token to revoke. Must belong to the calling user. |
Request example
{
"expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
}Example request — cURL
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
{
"success": true,
"data": {
"revoked": true
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 400 | VALIDATION_ERROR | Request 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.
/deals/{id}/templateCurrent template progress on a deal
Returns the template (if any) and the status of each step.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Example request — cURL
curl -X GET 'https://app.upstreamcrm.com/api/v1/deals/{id}/template' \
-H 'Authorization: Bearer <ACCESS_TOKEN>'Response 200 OK
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
/deals/{id}/templateAttach or detach a template from a deal
Pass `templateId: null` to detach the current template.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Request body
| Name | Type | Description |
|---|---|---|
| templateIdrequired | uuid | null | Template to apply, or `null` to detach. |
Request example
{
"templateId": "t1"
}Example request — cURL
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
{
"success": true,
"data": {
"templateId": "t1",
"nextPendingStep": {
"stepIndex": 1,
"type": "call",
"title": "Discovery call"
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |
/deals/{id}/template/schedule-nextSchedule the next pending template step as an activity
Creates the activity for the deal's next pending step, using the supplied `dueDate`.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | uuid | Deal id. |
Request body
| Name | Type | Description |
|---|---|---|
| dueDaterequired | ISO 8601 | When the next step should be due. |
| notes | string ≤ 10000 | null | Optional notes on the new activity. |
Request example
{
"dueDate": "2026-05-15T14:00:00.000Z"
}Example request — cURL
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
{
"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
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, invalid or expired access token. |
| 403 | FORBIDDEN | Token is valid but the user is not a member of the target organization. |
| 404 | NOT_FOUND | The requested resource does not exist or is not visible to the caller. |
| 400 | VALIDATION_ERROR | Request body or query failed Zod schema validation. `details[]` lists each offending field. |