API reference

Manage clients, onboardings, templates, and responses through 7 scoped REST API endpoints with offset and page-based pagination and per-plan rate limits.

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 > Integrations > API keys. 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.

ScopeGrants access to
clients:readList and get clients
clients:writeCreate clients
onboardings:readList and get onboardings
onboardings:writeCreate and update onboardings
templates:readList and get templates
templates:writeCreate templates
responses:readList responses for an onboarding

Base URL

https://app.portico.run/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 and Co",
    "tier": "pro"
  },
  "limits": {
    "onboardings_per_month": 100,
    "api_rate_limit": "200 requests/minute"
  }
}

onboardings_per_month is a number for capped plans or "unlimited" for Business.

Rate limits

PlanPer keyPer key and IP
Pro200 requests/min100 requests/min
Business1,000 requests/min500 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:

CodeMeaning
400Invalid or missing request parameters
401Missing or invalid API key
403Key lacks the required scope, or your plan does not include API access
404Resource not found or does not belong to your team
409Conflict (for example, duplicate email or invalid status transition)
429Rate limit exceeded
500Server error — retry the request

Pagination

List endpoints return a total count and accept limit (1 to 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 and 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 and 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 and 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.

Supported locales: en-US, en-GB, de, fr, it, es, pt, ja, zh

Response (201):

{
  "client": {
    "id": "uuid",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "company": "Smith and Co",
    "phone": "+1-555-0100",
    "created_at": "2026-03-15T10:30:00Z"
  }
}

Errors:

CodeError
400name and email are required
400Invalid email address
409Client 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:

CodeError
400template_id and client_id are required
403Monthly onboarding limit reached
404Template not found
404Client 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:

FromTo
draftcancelled
sentcancelled
sentin_progress
in_progresscancelled

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:

CodeError
400No valid fields to update
404Onboarding not found
409Could 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"
}
FieldTypeDescription
forcebooleanBypass the active-client safety check (default false). Required when the client has responded within the last 5 minutes.
pinstringOptional 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:

CodeError
400No client associated with this onboarding
400PIN must be exactly 6 digits
404Onboarding not found
409Client is actively working on this onboarding. Set force: true to resend.
409Completed, cancelled, or expired onboardings must be reopened before sending
502Could 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
          }
        ]
      }
    ]
  }
}

Create a template

POST /api/v1/templates

Scope: templates:write

Request body:

{
  "name": "New Client Intake",
  "industry": "agency"
}

name is required. industry is optional.

This creates an empty template with no steps or fields. Use the template builder in the UI to add form content, or build steps and fields through subsequent API calls.

Response (201):

{
  "template": {
    "id": "uuid",
    "name": "New Client Intake",
    "industry": "agency",
    "created_at": "2026-03-15T10:30:00Z",
    "updated_at": "2026-03-15T10:30:00Z"
  }
}

Errors:

CodeError
400name is required

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:

ParamDescription
limit1 to 100, default 25.
typeFilter by event type (see table below).

Scope: Any valid API key.

Event types:

TypeDescription
onboarding.completedClient finished all required steps.
onboarding.sentInvite email delivered to client.
onboarding.startedClient opened the onboarding link.
onboarding.overdueOverdue workflow triggered.
response.submittedClient submitted a field response.
signature.signedClient signed a signature field.
payment.succeededClient payment processed.
file.uploadedClient uploaded a file.
webhook.deliveredOutgoing webhook delivered to your endpoint.

Response (200):

{
  "events": [
    {
      "id": "uuid",
      "type": "onboarding.completed",
      "timestamp": "2026-03-20T14:00:00Z",
      "onboarding_id": "uuid",
      "data": {}
    }
  ]
}

Errors:

CodeError
400Unknown event type. Allowed: ...

Use the Events endpoint as a fallback alongside webhooks. If a webhook delivery fails, you can still poll events to catch what you missed.

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
}

Frequently asked questions

Which plan includes API access?
The REST API is available on the Pro plan ($79/month) and above.
How is the API authenticated?
API requests are authenticated with a scoped API key generated in Settings. Keys use Bearer token authentication.
Are there rate limits?
Yes. Rate limits vary by plan. Check the rate limits section for your plan's per-minute and per-day limits.