Developer Zone

Build On OPVII.

A simple, scoped REST API to sync contacts and events, record registrations, and trigger automated flows, straight from your own apps. Everything is JSON under one versioned base URL, authenticated with a bearer token you generate and revoke yourself. Successful calls wrap the payload in a data key, list endpoints add a meta block for pagination, and errors come back with the matching HTTP status. Recording a registration de-dupes the contact, resolves the event, and enrolls it into your default trigger flow in a single call.

Scoped Bearer Tokens JSON Only 120 Requests A Minute

Bearer

Scoped, Revocable Keys

Auth Header

9 Scopes

Granted One By One

Per API Key

120 / Min

Rate Limit

Then A 429

Webhooks

Events Pushed To You

Set In The App

Getting Started

Three Steps To Your First Successful Call.

  1. Step 1

    Create A Key

    In the app, open Settings → Developers → API Keys and click Create API Key. Choose only the scopes you need.

  2. Step 2

    Copy The Token

    The full token is shown once. It looks like opvii_{publicId}_{secret}. Store it somewhere safe.

  3. Step 3

    Make A Call

    Hit GET /ping with your token to confirm it works, then start syncing data.

Authentication

Every Request Must Carry A Valid API Key. Tokens Are Tenant-Scoped, So They Only Ever See Your Account's Data. Send The Token One Of Two Ways:

Bearer Token Recommended

Authorization: Bearer opvii_yourkeyid_yoursecret

API Key Header

X-Api-Key: opvii_yourkeyid_yoursecret

Tokens Have The Shape opvii_{publicId}_{secret}. The publicId Is A Non-Secret Lookup Handle; The secret Is Never Stored And Cannot Be Recovered. If You Lose It, Revoke The Key And Create A New One. A Missing Or Invalid Token Returns 401; A Valid Key Without The Required Scope Returns 403.

Base URL

All Endpoints Are Versioned Under /api/v1 And Return JSON.

http://opvii.localhost:8100/api/v1

Rate Limits

Each API Key Is Limited To 120 Requests Per Minute. Exceeding It Returns A 429 Too Many Requests. Back Off And Retry After A Short Pause.

Responses & Errors

Successful Responses Wrap The Payload In A data Key (List Endpoints Add A meta Block For Pagination). Errors Return An error Message With The Matching HTTP Status.

Success Envelope

{ "data": { "id": 482, "email": "jane@example.com" } }

Error Envelope

{ "error": "This API key is missing the required scope.",
  "required_scope": "contacts:write" }
Status Meaning
200 OK: request succeeded (or matched an existing record on an upsert).
201 Created: a new contact, event, registration, or enrollment was created.
401 Unauthorized: missing or invalid API key.
403 Forbidden: the key lacks the required scope (see required_scope).
404 Not Found: the referenced event or flow is not your tenant's.
422 Unprocessable: validation failed (e.g. no email or phone, or an inactive flow).
429 Too Many Requests: you exceeded 120 requests per minute.

Scopes

A Key Only Grants The Scopes You Assign It. Calls Outside Those Scopes Return 403. Grant The Minimum Each Integration Needs.

Scope Allows
contacts:read Read Contacts. List and search contacts.
contacts:write Create & Update Contacts. Create or update contacts (matched by email, then phone).
events:read Read Events. List events with registration counts.
events:write Create & Update Events. Create events.
registrations:write Create Registrations. Record a registration: creates/dedupes the contact, resolves the event, and enrolls into your default trigger flow.
flows:trigger Trigger Flows. Enroll a contact into a specific flow by id.
flows:read Read Flows. List flows and inspect a flow's enrollments.
messages:read Read Message Log. List the outbound SMS/email message log.
webhooks:manage Manage Webhooks. Reserved for webhook management. Endpoints are configured in the app under Developers → Webhooks.

Endpoints

Every Path Below Is Relative To The Base URL http://opvii.localhost:8100/api/v1.

Ping

Auth smoke-test. Confirms the key works and echoes the tenant name and granted scopes. Requires no scope, so any valid key works.

cURL

curl http://opvii.localhost:8100/api/v1/ping \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": {
    "ok": true,
    "tenant": "Northwind Financial",
    "key": "Website Integration",
    "scopes": ["contacts:write", "registrations:write"]
  }
}

List Contacts

Paginated, tenant-scoped list of contacts. Optional query string: q (search), per_page (default 25, max 100).

Parameters

q query · string Search term (name, email, phone).
per_page query · integer Results per page. Default 25, max 100.

cURL

curl "http://opvii.localhost:8100/api/v1/contacts?q=jane&per_page=25" \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": [
    {
      "id": 482,
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane@example.com",
      "phone": "+16025551234",
      "consent_sms": true,
      "opted_out": false,
      "source": "api",
      "created_at": "2026-06-20T17:04:11+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1
  }
}

Create Or Update A Contact

Creates a contact, or updates the existing one matched by email (then phone) within your tenant. At least one of email or phone is required. Returns 201 when created, 200 when an existing contact was updated.

Request Body

first_name string Max 255 characters.
last_name string Max 255 characters.
email string Valid email. Required if no phone is given.
phone string Normalized to E.164. Required if no email is given.

cURL

curl -X POST http://opvii.localhost:8100/api/v1/contacts \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "phone": "+1 (602) 555-1234"
  }'

Example Response

{
  "data": {
    "id": 482,
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "phone": "+16025551234",
    "consent_sms": false,
    "opted_out": false,
    "source": "api",
    "created_at": "2026-06-27T15:22:09+00:00"
  }
}

Get One Contact

Fetch a single contact by id. Tenant-scoped, so a contact that is not yours returns 404.

Parameters

id Required path · integer The contact id.

cURL

curl http://opvii.localhost:8100/api/v1/contacts/482 \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": {
    "id": 482,
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "phone": "+16025551234",
    "consent_sms": true,
    "opted_out": false,
    "source": "api",
    "created_at": "2026-06-20T17:04:11+00:00"
  }
}

Update One Contact

Update a single contact's identity fields. Only the fields you send are changed. Tenant-scoped, returning 404 if not yours.

Request Body

first_name string Max 255 characters.
last_name string Max 255 characters.
email string Valid email.
phone string Normalized to E.164.

cURL

curl -X PUT http://opvii.localhost:8100/api/v1/contacts/482 \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{ "last_name": "Smith" }'

Example Response

{
  "data": {
    "id": 482,
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "jane@example.com",
    "phone": "+16025551234",
    "consent_sms": true,
    "opted_out": false,
    "source": "api",
    "created_at": "2026-06-20T17:04:11+00:00"
  }
}

Delete One Contact

Soft-delete a single contact. Tenant-scoped, returning 404 if not yours.

Request Body

id Required path · integer The contact id.

cURL

curl -X DELETE http://opvii.localhost:8100/api/v1/contacts/482 \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": { "deleted": true, "id": 482 }
}

List Events

Paginated, tenant-scoped list of events, each with a live registered_count. Optional query string: per_page (default 25, max 100).

Parameters

per_page query · integer Results per page. Default 25, max 100.

cURL

curl "http://opvii.localhost:8100/api/v1/events?per_page=25" \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": [
    {
      "id": 17,
      "title": "Retirement Planning Seminar",
      "starts_at": "2026-07-15T18:00:00+00:00",
      "city": "Phoenix",
      "location": "Training Center A",
      "status": "scheduled",
      "capacity": 40,
      "registered_count": 23,
      "created_at": "2026-06-10T09:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1
  }
}

Create An Event

Creates a scheduled event. Only title is required. Returns 201 on success.

Request Body

title Required string Required. Max 255 characters.
starts_at datetime Any parseable date/time.
city string Max 120 characters.
location string Max 255 characters.
capacity integer Minimum 0.

cURL

curl -X POST http://opvii.localhost:8100/api/v1/events \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Retirement Planning Seminar",
    "starts_at": "2026-07-15 18:00",
    "city": "Phoenix",
    "location": "Training Center A",
    "capacity": 40
  }'

Example Response

{
  "data": {
    "id": 17,
    "title": "Retirement Planning Seminar",
    "starts_at": "2026-07-15T18:00:00+00:00",
    "city": "Phoenix",
    "location": "Training Center A",
    "status": "scheduled",
    "capacity": 40,
    "registered_count": 0,
    "created_at": "2026-06-27T15:22:09+00:00"
  }
}

Get One Event

Fetch a single event by id, with a live registered_count. Tenant-scoped, returning 404 if not yours.

Parameters

id Required path · integer The event id.

cURL

curl http://opvii.localhost:8100/api/v1/events/17 \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": {
    "id": 17,
    "title": "Retirement Planning Seminar",
    "starts_at": "2026-07-15T18:00:00+00:00",
    "city": "Phoenix",
    "location": "Training Center A",
    "status": "scheduled",
    "capacity": 40,
    "registered_count": 23,
    "created_at": "2026-06-10T09:00:00+00:00"
  }
}

Update One Event

Update an event. title is required. When starts_at is set or changed, the event's registrations are enrolled into your default trigger flow and any existing reminders are rescheduled to the new date. Tenant-scoped, returning 404 if not yours.

Request Body

title Required string Required. Max 255 characters.
starts_at datetime Any parseable date/time. Changing it (re)starts the flow.
city string Max 120 characters.
location string Max 255 characters.
capacity integer Minimum 0.
status string One of scheduled, completed, cancelled.

cURL

curl -X PUT http://opvii.localhost:8100/api/v1/events/17 \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Retirement Planning Seminar",
    "starts_at": "2026-07-20 18:00",
    "capacity": 50
  }'

Example Response

{
  "data": {
    "id": 17,
    "title": "Retirement Planning Seminar",
    "starts_at": "2026-07-20T18:00:00+00:00",
    "city": "Phoenix",
    "location": "Training Center A",
    "status": "scheduled",
    "capacity": 50,
    "registered_count": 23,
    "created_at": "2026-06-10T09:00:00+00:00"
  }
}

Mark Attendance

Mark which registrations attended. Pass the array of registration ids that attended; every other registration on the event is marked not-attended. Tenant-scoped, returning 404 if not yours.

Request Body

attended Required array · integer Registration ids that attended. Send an empty array to clear attendance.

cURL

curl -X POST http://opvii.localhost:8100/api/v1/events/17/attendance \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{ "attended": [904, 905, 912] }'

Example Response

{
  "data": { "event_id": 17, "attended_count": 3 }
}

Record A Registration

The headline "trigger a flow on registration" path. Creates or dedupes the contact, resolves (or creates) the event, records the registration, and enrolls the contact into your tenant's default trigger flow. Idempotent. At least one of email or phone is required, and you must reference the event with event_id, or event_title and/or event_date. Returns 201 when newly created, 200 when it matched an existing registration.

Request Body

email string Valid email. Required if no phone is given.
phone string Normalized to E.164. Required if no email is given.
first_name string Max 255 characters.
last_name string Max 255 characters.
event_id integer An existing event id. Wins over the title/date fields. 404 if not your tenant's.
event_title string Event title to match or create. Max 255 characters.
event_date datetime Event date/time.
event_city string Max 120 characters.

cURL

curl -X POST http://opvii.localhost:8100/api/v1/registrations \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "first_name": "Jane",
    "event_title": "Retirement Planning Seminar",
    "event_date": "2026-07-15 18:00"
  }'

Example Response

{
  "data": {
    "created": true,
    "enrolled": true,
    "registration": {
      "id": 904,
      "registered_at": "2026-06-27T15:22:09+00:00"
    },
    "contact": {
      "id": 482,
      "email": "jane@example.com",
      "phone": null,
      "name": "Jane Doe"
    },
    "event": {
      "id": 17,
      "title": "Retirement Planning Seminar",
      "starts_at": "2026-07-15T18:00:00+00:00"
    }
  }
}

List Flows

List your tenant's flows (sequences), each with its channel mode, active/default flags, and step + enrollment counts.

cURL

curl http://opvii.localhost:8100/api/v1/flows \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": [
    {
      "id": 8,
      "name": "Post-Seminar Followup",
      "channel_mode": "both",
      "active": true,
      "is_default": false,
      "steps_count": 4,
      "enrollments_count": 132
    }
  ]
}

List A Flow's Enrollments

Paginated list of a flow's enrollments, each with the contact summary and status. The {sequence} id is tenant-scoped, returning 404 if not yours. Optional query string: per_page (default 25, max 100).

Parameters

per_page query · integer Results per page. Default 25, max 100.

cURL

curl "http://opvii.localhost:8100/api/v1/flows/8/enrollments?per_page=25" \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret"

Example Response

{
  "data": [
    {
      "id": 1330,
      "status": "active",
      "current_step_order": 2,
      "next_run_at": "2026-06-28T15:22:09+00:00",
      "created_at": "2026-06-27T15:22:09+00:00",
      "contact": {
        "id": 482,
        "name": "Jane Doe",
        "email": "jane@example.com",
        "phone": "+16025551234"
      }
    }
  ],
  "meta": { "current_page": 1, "per_page": 25, "total": 1, "last_page": 1 }
}

Enroll Into A Specific Flow

Enroll a contact (found or created by email) into a SPECIFIC flow by its id. The {sequence} id is tenant-scoped, so a flow that is not yours returns 404, and an inactive flow returns 422. Returns 201 when a new enrollment is created, 200 when one already existed.

Request Body

email Required string Required. Valid email; the contact is found or created by it.
first_name string Used only when creating a new contact.
last_name string Used only when creating a new contact.
phone string Normalized to E.164. Used only when creating a new contact.

cURL

curl -X POST http://opvii.localhost:8100/api/v1/flows/8/enroll \
  -H "Authorization: Bearer opvii_yourkeyid_yoursecret" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "first_name": "Jane"
  }'

Example Response

{
  "data": {
    "enrolled": true,
    "flow": { "id": 8, "name": "Post-Seminar Followup" },
    "contact": { "id": 482, "email": "jane@example.com" },
    "enrollment": {
      "id": 1330,
      "status": "active",
      "next_run_at": "2026-06-28T15:22:09+00:00"
    }
  }
}

Ready To Build?

Create A Free Account, Generate A Scoped API Key In Settings → Developers, And Make Your First Call In Minutes.