API Reference
REST API endpoints for clients, onboardings, templates, responses, and events.
Pro plan and above.
The Portico API lets you create clients, launch onboardings, and read responses programmatically. All endpoints use JSON and follow REST conventions.
Authentication
Every request must include your API key in the Authorization header:
Authorization: Bearer pk_your-api-key
Generate keys in Settings > API & Webhooks. Keys are shown once on creation — store them securely. You can have up to 5 active keys per team.
Scopes
Each key can be restricted to specific scopes. If no scopes are set, the key has full access.
| Scope | Grants access to |
|---|---|
clients:read | List and get clients |
clients:write | Create clients |
onboardings:read | List and get onboardings |
onboardings:write | Create and update onboardings |
templates:read | List and get templates |
templates:write | Reserved for future use |
responses:read | List responses for an onboarding |
Base URL
https://app.portico.com/api/v1
Account info
Get current team
GET /api/v1/me
Returns team details and plan limits for the authenticated API key. Useful for verifying credentials and checking usage caps.
Scope: Any valid API key.
Response (200):
{
"team": {
"id": "uuid",
"name": "Smith & Co",
"tier": "pro"
},
"limits": {
"onboardings_per_month": 50,
"api_rate_limit": "200 requests/minute"
}
}
onboardings_per_month is a number for capped plans or "unlimited" for Business.
Rate limits
| Plan | Per key | Per key + IP |
|---|---|---|
| Pro | 200 requests/min | 100 requests/min |
| Business | 1,000 requests/min | 500 requests/min |
When you exceed the limit, the API returns 429 with a Retry-After: 60 header.
Error format
All errors return a JSON object with an error field:
{
"error": "Description of what went wrong"
}
Common status codes:
| Code | Meaning |
|---|---|
| 400 | Invalid or missing request parameters |
| 401 | Missing or invalid API key |
| 403 | Key lacks the required scope, or your plan doesn't include API access |
| 404 | Resource not found or doesn't belong to your team |
| 409 | Conflict (e.g., duplicate email, invalid status transition) |
| 429 | Rate limit exceeded |
| 500 | Server error — retry the request |
Pagination
List endpoints return a total count and accept limit (1–100, default 50). Clients and onboardings use offset-based pagination (offset, default 0). Templates and responses use page-based pagination (page, starts at 1).
Offset-based example:
{
"clients": [...],
"total": 142
}
Page-based example:
{
"templates": [...],
"total": 5,
"page": 1,
"limit": 50
}
Clients
List clients
GET /api/v1/clients
Query parameters: limit, offset, email
Filter by email for an exact (case-insensitive) match.
Scope: clients:read
Response (200):
{
"clients": [
{
"id": "uuid",
"name": "Jane Smith",
"email": "jane@example.com",
"company": "Smith & Co",
"phone": null,
"notes": null,
"created_at": "2026-03-15T10:30:00Z"
}
],
"total": 42
}
Get a client
GET /api/v1/clients/:id
Scope: clients:read
Response (200):
{
"client": {
"id": "uuid",
"name": "Jane Smith",
"email": "jane@example.com",
"company": "Smith & Co",
"phone": null,
"notes": null,
"created_at": "2026-03-15T10:30:00Z"
}
}
Create a client
POST /api/v1/clients
Scope: clients:write
Request body:
{
"name": "Jane Smith",
"email": "jane@example.com",
"company": "Smith & Co",
"phone": "+1-555-0100",
"notes": "Referred by Alex",
"locale": "en-US",
"date_format": "MM/DD/YYYY",
"timezone": "America/New_York"
}
name and email are required. All other fields are optional and default to your team's settings.
Field limits: name 200 chars, company 200 chars, phone 50 chars, notes 2,000 chars.
Response (201):
{
"client": {
"id": "uuid",
"name": "Jane Smith",
"email": "jane@example.com",
"company": "Smith & Co",
"phone": "+1-555-0100",
"created_at": "2026-03-15T10:30:00Z"
}
}
Errors:
| Code | Error |
|---|---|
| 400 | name and email are required |
| 400 | Invalid email address |
| 409 | Client with this email already exists |
Onboardings
List onboardings
GET /api/v1/onboardings
Query parameters: limit, offset, status, client_id
status accepts: draft, sent, in_progress, completed, cancelled.
Scope: onboardings:read
Response (200):
{
"onboardings": [
{
"id": "uuid",
"status": "in_progress",
"progress_pct": 60,
"created_at": "2026-03-15T10:30:00Z",
"due_date": "2026-04-01T00:00:00Z",
"assigned_to": "uuid",
"clients": {
"id": "uuid",
"name": "Jane Smith",
"email": "jane@example.com"
},
"templates": {
"id": "uuid",
"name": "Agency Onboarding"
}
}
],
"total": 18
}
Get an onboarding
GET /api/v1/onboardings/:id
Scope: onboardings:read
Returns the full onboarding including the template snapshot (the form structure captured when the onboarding was created).
Response (200):
{
"onboarding": {
"id": "uuid",
"status": "in_progress",
"progress_pct": 60,
"created_at": "2026-03-15T10:30:00Z",
"due_date": "2026-04-01T00:00:00Z",
"assigned_to": null,
"template_snapshot": {
"name": "Agency Onboarding",
"steps": [
{
"id": "uuid",
"title": "Business Details",
"sort_order": 0,
"is_optional": false,
"fields": [
{
"id": "uuid",
"type": "short_text",
"label": "Company Name",
"is_required": true,
"sort_order": 0,
"config": {}
}
]
}
]
},
"clients": {
"id": "uuid",
"name": "Jane Smith",
"email": "jane@example.com"
},
"templates": {
"id": "uuid",
"name": "Agency Onboarding"
}
}
}
Create an onboarding
POST /api/v1/onboardings
Scope: onboardings:write
Request body:
{
"template_id": "uuid",
"client_id": "uuid",
"due_date": "2026-04-01T00:00:00Z"
}
template_id and client_id are required. due_date is optional.
Creating an onboarding counts toward your monthly limit. If the limit is reached, the request returns 403.
Response (201):
{
"onboarding": {
"id": "uuid",
"status": "draft",
"progress_pct": 0,
"created_at": "2026-03-15T10:30:00Z",
"due_date": "2026-04-01T00:00:00Z",
"token": "hex-string"
}
}
Errors:
| Code | Error |
|---|---|
| 400 | template_id and client_id are required |
| 403 | Monthly onboarding limit reached |
| 404 | Template not found |
| 404 | Client not found |
Update an onboarding
PATCH /api/v1/onboardings/:id
Scope: onboardings:write
Request body (all fields optional):
{
"status": "cancelled",
"due_date": "2026-04-15T00:00:00Z",
"assigned_to": "uuid"
}
status only accepts cancelled or in_progress. Valid transitions:
| From | To |
|---|---|
| draft | cancelled |
| sent | cancelled |
| sent | in_progress |
| in_progress | cancelled |
Setting status to cancelled deactivates the client link and cancels pending reminders.
Response (200):
{
"onboarding": {
"id": "uuid",
"status": "cancelled",
"progress_pct": 60,
"due_date": "2026-04-15T00:00:00Z",
"assigned_to": null
}
}
Errors:
| Code | Error |
|---|---|
| 400 | No valid fields to update |
| 404 | Onboarding not found |
| 409 | Could not transition from "completed" to "cancelled" |
Send an onboarding
POST /api/v1/onboardings/:id/send
Scope: onboardings:write
Sends (or resends) the onboarding invite email to the client with a fresh magic-link token. The onboarding must be in draft, sent, or in_progress status.
Request body (optional):
{
"force": false,
"pin": "482901"
}
| Field | Type | Description |
|---|---|---|
force | boolean | Bypass the active-client safety check (default false). Required when the client has responded within the last 5 minutes. |
pin | string | Optional 6-digit PIN the client must enter before opening the form. |
If the onboarding is in_progress and the client has submitted a response in the last 5 minutes, the request returns 409 unless force: true is set.
Sending a draft onboarding transitions its status to sent. Prior access tokens are deactivated so only the newest link works.
Response (200):
{
"success": true
}
Errors:
| Code | Error |
|---|---|
| 400 | No client associated with this onboarding |
| 400 | PIN must be exactly 6 digits |
| 404 | Onboarding not found |
| 409 | Client is actively working on this onboarding. Set force: true to resend. |
| 409 | Completed, cancelled, or expired onboardings must be reopened before sending |
| 502 | Could not send the email. Please try again. |
Templates
List templates
GET /api/v1/templates
Query parameters: page, limit
page— starts at 1, default 1.limit— 1 to 100, default 50.
Scope: templates:read
Response (200):
{
"templates": [
{
"id": "uuid",
"name": "Agency Onboarding",
"industry": "agency",
"created_at": "2026-02-10T08:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}
],
"total": 5,
"page": 1,
"limit": 50
}
Get a template
GET /api/v1/templates/:id
Scope: templates:read
Returns the template with all steps and fields.
Response (200):
{
"template": {
"id": "uuid",
"name": "Agency Onboarding",
"industry": "agency",
"created_at": "2026-02-10T08:00:00Z",
"updated_at": "2026-03-01T12:00:00Z",
"template_steps": [
{
"id": "uuid",
"title": "Business Details",
"sort_order": 0,
"is_optional": false,
"conditional_rules": null,
"template_fields": [
{
"id": "uuid",
"type": "short_text",
"label": "Company Name",
"is_required": true,
"sort_order": 0,
"config": {},
"conditional_rules": null
}
]
}
]
}
}
Responses
List responses
GET /api/v1/responses?onboarding_id=uuid
Query parameters: onboarding_id (required), page, limit
page— starts at 1, default 1.limit— 1 to 100, default 50.
Scope: responses:read
Response (200):
{
"responses": [
{
"id": "uuid",
"field_id": "uuid",
"value": "Acme Corp",
"status": "approved",
"rejection_reason": null,
"created_at": "2026-03-20T14:00:00Z"
}
],
"total": 12,
"page": 1,
"limit": 50
}
Response status is one of: draft, submitted, approved, rejected, changes_requested.
Events
List events
GET /api/v1/events
Returns recent audit events for your team in reverse chronological order. Use this to poll for activity since your last sync.
Query parameters:
| Param | Description |
|---|---|
limit | 1 to 100, default 25. |
type | Filter by event type (see table below). |
Scope: Any valid API key.
Event types:
| Type | Description |
|---|---|
onboarding.completed | Client finished all required steps |
onboarding.sent | Invite email delivered to client |
onboarding.started | Client opened the onboarding link |
onboarding.overdue | Overdue workflow triggered |
response.submitted | Client submitted a field response |
signature.signed | Client signed a signature field |
payment.succeeded | Client payment processed |
file.uploaded | Client uploaded a file |
webhook.delivered | Outgoing webhook delivered |
Response (200):
{
"events": [
{
"id": "uuid",
"type": "onboarding.completed",
"timestamp": "2026-03-20T14:00:00Z",
"onboarding_id": "uuid",
"data": {}
}
]
}
Errors:
| Code | Error |
|---|---|
| 400 | Unknown event type. Allowed: ... |
Zapier subscriptions
Manage webhook subscriptions for Zapier triggers. See Webhooks for event types and payload details.
Subscribe
POST /api/v1/zapier/subscribe
Request body:
{
"event": "onboarding.completed",
"target_url": "https://hooks.zapier.com/hooks/catch/123/abc"
}
target_url must be a public HTTPS URL.
Allowed events: onboarding.completed, onboarding.sent, onboarding.started, onboarding.overdue, response.submitted, signature.signed, payment.succeeded, file.uploaded
Response (201):
{
"id": "uuid"
}
Unsubscribe
DELETE /api/v1/zapier/subscribe
Request body:
{
"id": "uuid"
}
Response (200):
{
"success": true
}