Documentation
LinkyCal homeCreate a LinkyCal account
Developer Documentation

LinkyCal API Documentation

Everything you need to integrate forms, booking, and contact management into your product.

OpenAPI specView llms.txt

Installation

The fastest way to add LinkyCal to your site is with the embeddable widgets. Drop in a script tag and initialize with your project slug.

Booking Widgethtml
<!-- Booking Widget -->
<script src="https://cdn.linkycal.com/widgets/booking.js"></script>
<script>
  LinkyCal.booking({
    projectSlug: "your-project",
    container: "#booking-widget"
  });
</script>
Form Widgethtml
<!-- Form Widget -->
<script src="https://cdn.linkycal.com/widgets/form.js"></script>
<script>
  LinkyCal.form({
    projectSlug: "your-project",
    formSlug: "contact",
    container: "#form-widget"
  });
</script>

Quick Start

Get up and running in four steps:

  1. Create a project in the dashboard
  2. Create an event type or form
  3. Use the anonymous visitor endpoints in your public form or booking UI
  4. For server-side management, create a project API key under MCP & APIs
Check available slotsbash
# Check available slots
curl "https://linkycal.com/api/v1/availability/your-project?date=2026-08-12&timezone=UTC&eventTypeSlug=consultation"

How It Works

Every interaction with LinkyCal follows a straightforward request/response flow:

  1. Client sends request to LinkyCal API
  2. Server validates input and checks availability/form config
  3. Action is performed (booking created, form step submitted, etc.)
  4. Response returned with result
  5. Optional: workflow triggers fire (email, webhook, tag)
JSON errors include a descriptive error field and may include a stable code. Successful responses vary by operation: JSON resources, file bodies, and empty 204 responses are all used where appropriate.

API Families

Visitor API · /api/v1/*

Canonical anonymous endpoints for availability, bookings, and page form submissions. These routes are safe for visitor-side code and are rate limited by IP.

Share-link API · /api/public/*

Public link resolution and form configuration used by LinkyCal share pages. The response-creation and step-submission variants are retained for compatibility; new direct integrations should prefer /api/v1/*.

Management API · /api/projects/:projectId/*

Server-side project administration with a project-scoped API key. Never expose this credential in a browser, widget, or public form.

Endpoint Catalog

Anonymous visitor endpoints cover form responses, file uploads, availability, booking creation, widgets, and public link resolution. They are rate limited and need no credential. Never put an API key in visitor-side code.

The protected management API uses the canonical /api/projects/:projectId/* routes. Send Authorization: Bearer lc_live_... from a server, secure automation, or local agent. The key must belong to the project ID in the URL and the project must have API access on its current plan.

The complete machine-readable operation list and security declaration are in the OpenAPI 3.1 specification. Agent-oriented integration guidance is available in llms.txt.

Dashboard-only Operations

Project creation and deletion, members, API-key management, billing, and OAuth connections are dashboard-only. Account, team, and onboarding operations are also session-only. API keys receive api_key_route_forbidden for these operations; use the dashboard instead.

Create Response

POST /api/v1/forms/:projectSlug/:formSlug/responses

Start a new form response. Returns the response object and full form config. Each step is one page. Step settings.pageLayout and form settings.transition live on the existing settings JSON.

PropertyTypeRequiredDescription
projectSlugstringYesYour project's URL slug (path param)
formSlugstringYesThe form's URL slug (path param)
Requestbash
curl -X POST "https://linkycal.com/api/v1/forms/acme/contact/responses" \
  -H "Content-Type: application/json"
Responsejson
{
  "response": {
    "id": "resp_a1b2c3d4",
    "formId": "form_x1y2z3",
    "currentStepIndex": 0,
    "status": "in_progress"
  },
  "form": {
    "id": "form_x1y2z3",
    "name": "Contact Form",
    "steps": [
      {
        "id": "step_1",
        "title": "Your Details",
        "fields": [
          { "id": "fld_1", "type": "text", "label": "Full Name", "required": true },
          { "id": "fld_2", "type": "email", "label": "Email", "required": true }
        ]
      }
    ]
  }
}

Submit Step

PATCH /api/v1/forms/:projectSlug/:formSlug/responses/:responseId/steps/:stepIndex

Submit field values for a specific page. Pages must be submitted in order (0, 1, 2...). Pass complete: true on the final visible page to mark the response completed.

PropertyTypeRequiredDescription
fieldsarrayYesArray of { fieldId, value } objects. File fields may include fileUrl from the upload endpoint.
completebooleanNoSet true on the last visible page to finalize the response. Required when the last page is conditional, because the server cannot infer the last page from the index alone.
Requestbash
curl -X PATCH "https://linkycal.com/api/v1/forms/acme/contact/responses/resp_a1b2c3d4/steps/0" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      { "fieldId": "fld_1", "value": "Jane Smith" },
      { "fieldId": "fld_2", "value": "jane@example.com" }
    ]
  }'
Responsejson
{
  "response": {
    "id": "resp_a1b2c3d4",
    "status": "in_progress",
    "currentStepIndex": 1,
    "completedAt": null
  }
}
If you PATCH only steps/0 and omit complete, an exploded form can stay in_progress. Send complete on the last visible page.

Upload File

POST /api/v1/forms/:projectSlug/:formSlug/responses/:responseId/uploads

Upload a file for a file field before submitting that step through the JSON API. The upload response returns a private file pointer that you include as fileUrl.

Upload requestbash
curl -X POST "https://linkycal.com/api/v1/forms/acme/contact/responses/resp_a1b2c3d4/uploads" \
  -F "fieldId=resume" \
  -F "file=@./resume.pdf"
Submit uploaded filebash
curl -X PATCH "https://linkycal.com/api/v1/forms/acme/contact/responses/resp_a1b2c3d4/steps/0" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      {
        "fieldId": "resume",
        "value": "resume.pdf",
        "fileUrl": "form-responses/project/form/response/resume/upload-id.pdf"
      }
    ]
  }'
Uploaded respondent files are private by default. Dashboard users open them from the response drawer. API consumers can download them with a project API key at GET /api/v1/forms/:projectSlug/:formSlug/responses/:responseId/files/:valueId.

Native HTML Form

POST /api/public/forms/:projectSlug/:formSlug/submit

Post a regular browser form directly to LinkyCal without any client-side JavaScript. LinkyCal returns a hosted thank-you page by default, or a redirect if you configure one in the form builder.

HTML Form Actionhtml
<form action="https://linkycal.com/api/public/forms/acme/contact/submit" method="post" enctype="multipart/form-data">
  <input type="text" name="full_name" required />
  <input type="email" name="email" required />
  <input type="file" name="resume" />
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
</form>
Use your form field IDs as the HTML input name values. You can get the exact IDs from the form builder or the generated form API prompt. Include enctype="multipart/form-data" when the form has file inputs.

Get Form Config

GET /api/widget/form/:projectSlug/:formSlug/config

Returns the full form structure with pages, fields, and validation rules. Each step is one page. This is the same endpoint used internally by the form widget.

Requestbash
curl "https://linkycal.com/api/widget/form/acme/contact/config"
Responsejson
{
  "form": {
    "id": "form_x1y2z3",
    "name": "Contact Form",
    "slug": "contact",
    "steps": [
      {
        "id": "step_1",
        "title": "Your Details",
        "fields": [
          { "id": "fld_1", "type": "text", "label": "Full Name", "required": true },
          { "id": "fld_2", "type": "email", "label": "Email", "required": true },
          { "id": "fld_3", "type": "phone", "label": "Phone", "required": false }
        ]
      }
    ]
  }
}

Other visitor endpoints

Supporting anonymous routes used by public links, widgets, analytics, and older form integrations.

GET
/api/v1/event-types/:projectSlug/:eventSlug

Get public event type configuration.

POST
/api/v1/t

Record an anonymous visitor analytics event.

GET
/api/public/resolve/:projectSlug/:slug

Resolve a share-link slug to a form or event type.

POST
/api/public/forms/:projectSlug/:formSlug/responses

Legacy form response creation.

PATCH
/api/public/forms/:projectSlug/:formSlug/responses/:responseId/steps/:stepIndex

Legacy form step submission.

POST
/api/public/forms/:projectSlug/:formSlug/submit

Legacy single-request form submission.

GET
/api/uploads/:key

Download a public upload by object key.

  • The /api/public form write routes are retained for compatibility. New direct integrations should use the equivalent /api/v1 form routes.
  • Public upload URLs contain unguessable object keys; do not treat them as authorization for sensitive private response files.

Check Availability

GET /api/v1/availability/:projectSlug

Returns available time slots for a given event type on a specific date. Use this to display available times to your users before creating a booking.

PropertyTypeRequiredDescription
datestringYesDate in YYYY-MM-DD format
timezonestringNoIANA timezone, defaults to UTC
eventTypeSlugstringYesThe event type's URL slug
Requestbash
curl "https://linkycal.com/api/v1/availability/acme?date=2026-08-12&timezone=UTC&eventTypeSlug=consultation"
Responsejson
{
  "slots": [
    { "start": "2026-03-24T09:00:00Z", "end": "2026-03-24T09:30:00Z" },
    { "start": "2026-03-24T10:00:00Z", "end": "2026-03-24T10:30:00Z" }
  ],
  "date": "2026-03-24",
  "timezone": "UTC"
}

Create Booking

POST /api/v1/bookings

Create a new booking for an available time slot. The system validates slot availability before confirming. A confirmation email is automatically sent to the guest.

PropertyTypeRequiredDescription
projectSlugstringYesYour project's URL slug
eventTypeSlugstringYesThe event type's URL slug
startTimestringYesISO 8601 datetime
namestringYesGuest name
emailstringYesGuest email
notesstringNoAdditional notes from the guest
timezonestringYesGuest timezone (IANA format, e.g. America/New_York)
formFieldsobjectNoValues for the event type's custom booking-form fields, keyed by field ID
Requestbash
curl -X POST "https://linkycal.com/api/v1/bookings" \
  -H "Content-Type: application/json" \
  -d '{
    "projectSlug": "acme",
    "eventTypeSlug": "consultation",
    "startTime": "2026-03-24T09:00:00Z",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "notes": "Looking forward to our meeting",
    "timezone": "America/New_York"
  }'
Responsejson
{
  "booking": {
    "id": "bk_abc123",
    "eventTypeId": "et_xyz789",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "startTime": "2026-03-24T09:00:00Z",
    "endTime": "2026-03-24T09:30:00Z",
    "timezone": "America/New_York",
    "status": "confirmed",
    "notes": "Looking forward to our meeting",
    "createdAt": "2026-03-23T20:00:00Z"
  }
}
A confirmation email is automatically sent to the guest. If Google Calendar is connected, an event is created.

Cancel Booking

PATCH /api/projects/:projectId/bookings/:bookingId/cancel

Cancel an existing booking. The booking status is set to cancelled. If connected to Google Calendar, the calendar event is also removed.

This protected project route accepts either a dashboard session or a project API key. Server-side integrations should use the Bearer header below.
Requestbash
curl -X PATCH "https://linkycal.com/api/projects/proj_123/bookings/bk_abc123/cancel" \
  -H "Authorization: Bearer lc_live_your_api_key"
Responsejson
{
  "booking": {
    "id": "bk_abc123",
    "status": "cancelled",
    "cancelledAt": "2026-03-24T08:00:00Z"
  }
}

List Contacts

GET /api/projects/:projectId/contacts

Retrieve the first page of contacts for a project, with optional search and tag filtering. The response includes the full filtered total; see Contact list filters for the complete filter and pagination reference.

PropertyTypeRequiredDescription
searchstringNoSearch by name or email
tagIdstringNoFilter by tag ID
limitintegerNoPage size, default 50 and maximum 100
offsetintegerNoZero-based result offset
Responsejson
{
  "contacts": [
    {
      "id": "ct_m1n2o3",
      "name": "Jane Smith",
      "email": "jane@example.com",
      "phone": "+1-555-0123",
      "tags": [
        { "id": "tag_lead", "name": "Lead", "color": "#1B4332" }
      ],
      "lastActivityAt": "2026-07-22T08:30:00.000Z",
      "createdAt": "2026-03-20T10:00:00.000Z"
    }
  ],
  "total": 1
}

Create Contact

POST /api/projects/:projectId/contacts

Create a new contact in your project. Contacts are also created automatically when a form is submitted or a booking is made.

PropertyTypeRequiredDescription
namestringYesContact's full name
emailstringNoContact's email address
phonestringNoContact's phone number
notesstringNoInternal notes about the contact
Requestbash
curl -X POST "https://linkycal.com/api/projects/proj_123/contacts" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer lc_live_your_api_key" \
  -d '{
    "name": "Jane Smith",
    "email": "jane@example.com",
    "phone": "+1-555-0123",
    "notes": "Met at conference"
  }'
Responsejson
{
  "contact": {
    "id": "ct_p4q5r6",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "phone": "+1-555-0123",
    "notes": "Met at conference",
    "createdAt": "2026-03-24T14:00:00Z"
  }
}

Update Contact

PUT /api/projects/:projectId/contacts/:contactId

Update an existing contact's information. Only provided fields are updated.

Requestbash
curl -X PUT "https://linkycal.com/api/projects/proj_123/contacts/ct_p4q5r6" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer lc_live_your_api_key" \
  -d '{
    "name": "Jane Smith-Doe",
    "notes": "Updated: now a paying customer"
  }'
Responsejson
{
  "contact": {
    "id": "ct_p4q5r6",
    "name": "Jane Smith-Doe",
    "email": "jane@example.com",
    "phone": "+1-555-0123",
    "notes": "Updated: now a paying customer",
    "updatedAt": "2026-03-24T15:00:00Z"
  }
}

Projects and entitlements

Read and update the project addressed by the API key, and inspect the plan limits that apply to it.

GET
/api/projects/:projectId

Get the current project.

PUT
/api/projects/:projectId

Update project settings and branding.

GET
/api/projects/:projectId/entitlements

Get the subscription, usage, and effective plan limits.

  • Project creation and deletion remain dashboard-only.
  • PUT accepts name, slug, timezone, and onboarded; slugs use lowercase letters, digits, and hyphens.
  • Entitlements are the source of truth for API access and resource limits; do not hard-code plan assumptions.

Event types

Manage bookable meeting definitions and choose which connected calendars each event type reads and writes.

GET
/api/projects/:projectId/event-types

List event types.

POST
/api/projects/:projectId/event-types

Create an event type.

GET
/api/projects/:projectId/event-types/:eventTypeId

Get an event type.

PUT
/api/projects/:projectId/event-types/:eventTypeId

Update an event type.

DELETE
/api/projects/:projectId/event-types/:eventTypeId

Delete an event type.

GET
/api/projects/:projectId/event-types/:eventTypeId/calendars

Get destination and availability calendar selections.

PUT
/api/projects/:projectId/event-types/:eventTypeId/calendars

Replace destination and availability calendar selections.

  • Event type writes are subject to the project plan limit.
  • Create with name, slug, duration, and optional description, location, color, buffers, booking caps, confirmation mode, bookingFormId, settings, or copyFromEventTypeId.
  • Calendar IDs must belong to a connection available to the project or its team.

Schedules and availability rules

Manage reusable schedules, their weekly working windows, and date-specific availability overrides.

GET
/api/projects/:projectId/schedules

List schedules.

POST
/api/projects/:projectId/schedules

Create a schedule.

PUT
/api/projects/:projectId/schedules/:scheduleId

Update a schedule.

DELETE
/api/projects/:projectId/schedules/:scheduleId

Delete a schedule.

GET
/api/projects/:projectId/schedules/:scheduleId/rules

List weekly availability rules.

PUT
/api/projects/:projectId/schedules/:scheduleId/rules

Replace weekly availability rules.

GET
/api/projects/:projectId/schedules/:scheduleId/overrides

List date overrides.

POST
/api/projects/:projectId/schedules/:scheduleId/overrides

Create a date override.

DELETE
/api/projects/:projectId/schedules/:scheduleId/overrides/:overrideId

Delete a date override.

  • Times in rules and overrides are local to the schedule timezone.
  • Create schedules with name, timezone, and isDefault. Rules contain dayOfWeek, startTime, and endTime; 24:00 is valid only as an end time.
  • Replacing rules uses the complete submitted rule set; read the current rules before editing.

Booking management

Read bookings created by visitor flows and perform organizer-side state transitions.

GET
/api/projects/:projectId/bookings

List project bookings.

GET
/api/projects/:projectId/bookings/:bookingId

Get a booking.

PATCH
/api/projects/:projectId/bookings/:bookingId/cancel

Cancel a booking and optionally record a reason.

PATCH
/api/projects/:projectId/bookings/:bookingId/confirm

Confirm a pending booking.

PATCH
/api/projects/:projectId/bookings/:bookingId/decline

Decline a pending booking.

GET
/api/projects/:projectId/bookings/:bookingId/form-response

Get the intake form response attached to a booking.

  • Confirmation and cancellation may update Google Calendar and send email.
  • Cancellation accepts an optional reason. Decline accepts an optional reason and notify flag; confirmation requires no request body.
  • The visitor booking creation endpoint is documented separately under Booking API.

Forms and responses

Manage forms, ordered steps and fields, submitted responses, and private response files.

GET
/api/projects/:projectId/forms

List forms.

POST
/api/projects/:projectId/forms

Create a form.

GET
/api/projects/:projectId/forms/:formId

Get a form with its structure.

PUT
/api/projects/:projectId/forms/:formId

Update a form.

DELETE
/api/projects/:projectId/forms/:formId

Delete a form.

GET
/api/projects/:projectId/forms/:formId/steps

List form steps.

POST
/api/projects/:projectId/forms/:formId/steps

Create a form step.

PUT
/api/projects/:projectId/forms/:formId/steps/:stepId

Update a form step.

DELETE
/api/projects/:projectId/forms/:formId/steps/:stepId

Delete a form step.

PUT
/api/projects/:projectId/forms/:formId/steps/reorder

Reorder form steps.

GET
/api/projects/:projectId/forms/:formId/fields

List form fields.

POST
/api/projects/:projectId/forms/:formId/fields

Create a form field.

PUT
/api/projects/:projectId/forms/:formId/fields/:fieldId

Update a form field.

DELETE
/api/projects/:projectId/forms/:formId/fields/:fieldId

Delete a form field.

PUT
/api/projects/:projectId/forms/:formId/fields/reorder

Reorder form fields.

GET
/api/projects/:projectId/forms/:formId/responses

List form responses.

GET
/api/projects/:projectId/forms/:formId/responses/:responseId

Get a form response.

GET
/api/projects/:projectId/forms/:formId/responses/:responseId/files/:valueId

Download a private response file.

GET
/api/v1/forms/:projectSlug/:formSlug/responses/:responseId/files/:valueId

Download a private response file through the project-scoped API-key route.

DELETE
/api/projects/:projectId/form-responses/:responseId

Delete a form response.

  • Reorder endpoints accept the complete ordered list of IDs.
  • Each form step is one page. New forms write type as single. Keep settings.pageLayout on the step settings JSON and settings.transition on the form settings JSON.
  • PATCH of only steps/0 without complete can leave an exploded form in_progress. Send complete on the last visible page.
  • Forms still return name, slug, type, status, and settings. Steps contain display and visibility data. Fields contain type, label, settings, options, contact mapping, and visibility.
  • Deleting a response removes its stored values and owned response files.

Contact management

Manage CRM records, saved list and Kanban views, activity, next actions, stages, imports, and enrichment.

GET
/api/projects/:projectId/contacts

List and filter contacts.

POST
/api/projects/:projectId/contacts

Create a contact.

POST
/api/projects/:projectId/contacts/import

Import mapped contact rows.

GET
/api/projects/:projectId/contacts/:contactId

Get a contact with tags.

PUT
/api/projects/:projectId/contacts/:contactId

Update a contact.

DELETE
/api/projects/:projectId/contacts/:contactId

Delete a contact.

GET
/api/projects/:projectId/contacts/:contactId/activities

List the cursor-paginated contact timeline and category counts.

PUT
/api/projects/:projectId/contacts/:contactId/next-action

Set, replace, or complete the contact's next action.

POST
/api/projects/:projectId/contacts/:contactId/stage

Move a contact between stage tags.

POST
/api/projects/:projectId/contacts/:contactId/enrich

Run contact research and return the enriched contact.

GET
/api/projects/:projectId/contact-views

List saved contact views.

POST
/api/projects/:projectId/contact-views

Create a saved contact view.

PUT
/api/projects/:projectId/contact-views/:viewId

Update a saved contact view.

DELETE
/api/projects/:projectId/contact-views/:viewId

Delete a saved contact view.

POST
/api/projects/:projectId/pipeline/seed

Create the default pipeline stages when missing.

  • Contact listing defaults to 50 records and returns { contacts, total }; use limit and offset to load more.
  • Supported filters include search, tagId, repeated tagIds, matchAllTags, stageTagId, repeated excludeStageTagIds, activityType, activitySinceDays, noActivitySinceDays, and bookingStatus.
  • Contact writes support name, email, phone, notes, metadata, company, companyWebsite, position, companySize, estimatedRevenue, and linkedinUrl. Set next action with { text, deadline }; send both as null to complete it.

Contact list filters

PropertyTypeRequiredDescription
searchstringNoMatch contact name or email
tagIdstringNoLegacy single-tag filter
tagIdsstring[]NoRepeat the query key to filter by multiple tags
matchAllTagsbooleanNoRequire every tagIds value instead of any
stageTagIdstringNoRequire the current stage tag
excludeStageTagIdsstring[]NoRepeated stage IDs to exclude
activityTypeenumNoFilter by the latest supported activity type
activitySinceDaysnumberNoRequire activity within this many days
noActivitySinceDaysnumberNoRequire no activity within this many days
bookingStatusenumNoFilter by related booking status
limitintegerNoPage size; defaults to 50 and is capped at 100
offsetintegerNoZero-based result offset
Filtered contact pagebash
curl --get "https://linkycal.com/api/projects/proj_123/contacts" \
  -H "Authorization: Bearer lc_live_your_api_key" \
  --data-urlencode "tagIds=tag_lead" \
  --data-urlencode "tagIds=tag_vip" \
  --data-urlencode "matchAllTags=true" \
  --data-urlencode "limit=50" \
  --data-urlencode "offset=0"

Contact activity pagination

Activity accepts category values all, bookings, form_responses, or workflows. The default page size is 20 and the maximum is 100. Pass the returned opaque nextCursor unchanged to load the next page.

Contact timeline pagebash
curl --get "https://linkycal.com/api/projects/proj_123/contacts/ct_123/activities" \
  -H "Authorization: Bearer lc_live_your_api_key" \
  --data-urlencode "category=bookings" \
  --data-urlencode "limit=20"

Tags

Create project tags and assign them to contacts. Names are unique within a project after trimming and case normalization.

GET
/api/projects/:projectId/tags

List tags with search and cursor pagination.

POST
/api/projects/:projectId/tags

Create a tag from name and optional #RRGGBB color.

GET
/api/projects/:projectId/tags/:tagId

Get a tag.

PATCH
/api/projects/:projectId/tags/:tagId

Update a tag's name or color.

DELETE
/api/projects/:projectId/tags/:tagId

Delete an unused tag.

POST
/api/projects/:projectId/contacts/:contactId/tags

Legacy assignment endpoint accepting { tagId }.

PUT
/api/projects/:projectId/contacts/:contactId/tags/:tagId

Assign a tag to a contact idempotently.

DELETE
/api/projects/:projectId/contacts/:contactId/tags/:tagId

Remove a tag from a contact idempotently.

  • List tags with search and optional cursor pagination. A cursor requires the same limit on the next request.
  • PUT assignment and DELETE removal are idempotent: assigned or removed is false when no relationship changed.
  • Deleting a workflow-referenced tag returns 409 TAG_IN_USE with the workflows that block deletion.
  • POST assignment is retained for compatibility; new integrations should use the canonical PUT endpoint.

List and create tags

PropertyTypeRequiredDescription
searchstringNoCase-insensitive name search, 1–100 characters
limitintegerNoOptional page size from 1 to 100
cursorstringNoOpaque nextCursor value; requires limit
namestringYesTrimmed project-unique name, 1–50 characters
colorstringNoOptional six-digit hexadecimal color such as #1B4332
Create a tagbash
curl -X POST "https://linkycal.com/api/projects/proj_123/tags" \
  -H "Authorization: Bearer lc_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"name":"Qualified lead","color":"#1B4332"}'
Paginated tag responsejson
{
  "tags": [
    {
      "id": "tag_123",
      "projectId": "proj_123",
      "name": "Qualified lead",
      "color": "#1B4332",
      "createdAt": "2026-07-22T09:00:00.000Z"
    }
  ],
  "nextCursor": null
}

Assign and remove contact tags

Idempotent assignmentbash
curl -X PUT \
  "https://linkycal.com/api/projects/proj_123/contacts/ct_123/tags/tag_123" \
  -H "Authorization: Bearer lc_live_your_api_key"

Assignment returns { tag, assigned }; removal returns { success, tag, removed }. Repeating either request succeeds and reports false for the unchanged relationship.

Deletion conflictjson
{
  "error": "Tag is referenced by one or more workflows",
  "code": "TAG_IN_USE",
  "workflows": [{ "id": "wf_123", "name": "Research new leads" }]
}

Workflow management

Manage workflow definitions and ordered steps, inspect execution runs, and start manual or test executions.

GET
/api/projects/:projectId/workflows

List workflows.

POST
/api/projects/:projectId/workflows

Create a workflow.

GET
/api/projects/:projectId/workflows/:workflowId

Get a workflow.

PUT
/api/projects/:projectId/workflows/:workflowId

Update a workflow.

DELETE
/api/projects/:projectId/workflows/:workflowId

Delete a workflow.

GET
/api/projects/:projectId/workflows/:workflowId/steps

List workflow steps.

POST
/api/projects/:projectId/workflows/:workflowId/steps

Create a workflow step.

PUT
/api/projects/:projectId/workflows/:workflowId/steps/:stepId

Update a workflow step.

DELETE
/api/projects/:projectId/workflows/:workflowId/steps/:stepId

Delete a workflow step.

PUT
/api/projects/:projectId/workflows/:workflowId/steps/reorder

Reorder workflow steps.

GET
/api/projects/:projectId/workflows/:workflowId/runs

List workflow runs with an optional limit.

GET
/api/projects/:projectId/workflows/:workflowId/runs/:runId

Get one run with step snapshots.

POST
/api/projects/:projectId/workflows/:workflowId/trigger

Start a manual workflow run.

POST
/api/projects/:projectId/workflows/:workflowId/test

Test a workflow against supplied context.

  • Triggers: form_submitted, booking_created, booking_cancelled, booking_pending, booking_confirmed, new_contact_created, tag_added, manual, and scheduled.
  • Step types: send_email, ai_research, add_tag, remove_tag, wait, condition, webhook, and update_contact.
  • Create a workflow with name, trigger, and optional triggerConfig. Steps use sortOrder, type, config, and an optional condition; update calls are partial.

Analytics and recent activity

Read project-level activity and unique-journey booking/form funnels, then configure validated customer analytics providers.

GET
/api/projects/:projectId/activity/recent

Get recent project activity.

GET
/api/projects/:projectId/analytics/filters

Get available analytics filter values.

GET
/api/projects/:projectId/analytics/overview

Get conversion and traffic overview metrics.

GET
/api/projects/:projectId/analytics/bookings

Get booking analytics.

GET
/api/projects/:projectId/analytics/forms

Get form analytics.

GET
/api/projects/:projectId/analytics/integrations

List normalized GA4, Meta Pixel, and PostHog configuration.

PUT
/api/projects/:projectId/analytics/integrations/:provider

Configure one allowlisted analytics provider.

  • Detailed analytics and provider configuration require a Pro or Business project. The Worker enforces entitlement and project scope for sessions, REST API keys, and MCP OAuth grants.
  • Analytics supports period=7d|30d|90d|custom. Custom requires both inclusive start and end ISO dates; preset periods omit them.
  • Queries may filter by resourceSlug, UTM source/medium/campaign, direct or widget source, and mobile/tablet/desktop device type.
  • Booking analytics additionally require an IANA timezone. Returned dates, weekdays, and times are grouped in that dashboard viewer timezone.
  • Choose one project-owned event type or form for exact stages. All resources remains a backward-compatible high-level summary.
  • Detailed reports count unique journeys and expose availableSince, stage visitors, continuation/drop-off rates, conditional skips, Journey sources, and Visitor devices.
  • Booking reports add UTC-backed clicked weekdays and selected-date slot checks plus booked weekday/time distributions from persisted booking requests. Every request counts once regardless of status, including rescheduled originals and replacements.
  • Persisted booking distributions follow period and event type filters; traffic and device filters remain visitor-journey dimensions because those fields are not stored on booking rows.
  • Reports never return names, emails, raw answers, journey IDs, IP addresses, or raw errors.
  • Provider writes accept only public GA4 measurement IDs, numeric Meta Pixel IDs, and PostHog project keys with an allowlisted US/EU host. Raw scripts and arbitrary URLs are rejected.

Files and project calendars

Upload project-owned files and inspect calendars available through the project's connected Google accounts.

POST
/api/projects/:projectId/uploads

Upload a project-owned file.

DELETE
/api/projects/:projectId/uploads/:key

Delete a project-owned upload.

GET
/api/projects/:projectId/calendar/calendars

List calendars available to the project.

  • Uploads use multipart/form-data and return an object key or URL for later use.
  • Deleting an upload requires the complete object key, including nested path segments.
  • Calendar listing is read-only; connect or disconnect Google accounts in the dashboard, then select calendars through the event type endpoints.

Booking Widget

Embed a fully functional booking experience on any page. The widget handles event type selection, date/time picking, and form submission.

Full Embedhtml
<div id="booking-widget"></div>
<script src="https://cdn.linkycal.com/widgets/booking.js"></script>
<script>
  LinkyCal.booking({
    projectSlug: "acme",
    container: "#booking-widget",
    eventTypeSlug: "consultation",
    theme: {
      primaryColor: "#1B4332"
    }
  });
</script>
PropertyTypeRequiredDescription
projectSlugstringYesYour project's URL slug
containerstring | HTMLElementYesCSS selector or DOM element
eventTypeSlugstringNoSkip event type selection
theme.primaryColorstringNoOverride brand color
hiddenRecord<string, string | string[]>NoPrefill hidden (or visible) fields by field id. Wins over host-page query params.
ui{ hideBanner?, hideBranding?, hideTitle?, hideIntro?, hideAvatar? }NoHide booking chrome for this embed. hideBranding needs Pro or Business. URL hide_* flags also work.

Form Widget

Embed page forms directly on your site. The widget renders each page in sequence and handles validation, submission, and completion state.

Full Embedhtml
<div id="form-widget"></div>
<script src="https://cdn.linkycal.com/widgets/form.js"></script>
<script>
  LinkyCal.form({
    projectSlug: "acme",
    formSlug: "contact",
    container: "#form-widget",
    theme: {
      primaryColor: "#1B4332"
    }
  });
</script>
PropertyTypeRequiredDescription
projectSlugstringYesYour project's URL slug
formSlugstringYesThe form's URL slug
containerstring | HTMLElementYesCSS selector or DOM element
theme.primaryColorstringNoOverride brand color
hiddenRecord<string, string | string[]>NoPrefill hidden (or visible) fields by field id. Wins over host-page query params.
ui{ hideBanner?, hideBranding?, hideTitle?, hideIntro?, hideMedia? }NoHide form chrome for this embed. hideBranding needs Pro or Business.

Prefill & hidden fields

Hidden fields stay off the public screen but still accept values. Keys are field ids (not labels). Query values win over a field's default. Widget hidden wins over the host page query string.

Share linktext
https://linkycal.com/acme/contact?utm_source=newsletter&plan=pro
Embed with JS valueshtml
<div id="form-widget"></div>
<script src="https://cdn.linkycal.com/widgets/form.js"></script>
<script>
  LinkyCal.form({
    projectSlug: "acme",
    formSlug: "contact",
    container: "#form-widget",
    hidden: {
      user_id: "usr_123",
      plan: "pro"
    }
  });
</script>

Form and booking widgets also forward the host page query string into the iframe (except embed, theme, lc_source, and lc_journey). So ?plan=pro on your site fills a plan field. Booking links also accept reserved name, email, and notes when those ids are free. File fields cannot be prefilled.

Hide page chrome

Form and event settings can hide banner, title, intro, section image, avatar, and LinkyCal branding. A share link or embed can hide more with query flags. A flag cannot unhide something the resource already hid. Hiding branding needs Pro or Business.

Share linktext
https://linkycal.com/acme/contact?hide_banner=1&hide_title=1
Widgetjavascript
LinkyCal.form({
  projectSlug: "acme",
  formSlug: "contact",
  container: "#form-widget",
  ui: { hideBanner: true, hideTitle: true }
});

Flags are hide_banner, hide_branding, hide_title, hide_intro, hide_avatar, and hide_media. Custom CSS can target [data-lc-banner], [data-lc-title], [data-lc-intro], [data-lc-avatar], [data-lc-media], and [data-lc-branding].

Customization

Both widgets accept a theme object to match your brand. Override colors, border radius, and fonts.

Theme Overridejavascript
LinkyCal.booking({
  projectSlug: "acme",
  container: "#booking-widget",
  theme: {
    primaryColor: "#1B4332",
    borderRadius: "12px",
    fontFamily: "Inter, sans-serif"
  }
});
Both widgets are zero-dependency IIFE bundles under 6KB gzipped.

Triggers

Workflows start with a trigger. When the trigger event occurs, all connected actions execute in sequence.

form_submittedFires when a form response is completed
booking_createdFires whenever a new booking is created
booking_cancelledFires when a booking is cancelled
booking_pending / booking_confirmedFires for the corresponding booking state
new_contact_createdFires when a brand-new contact is stored
tag_addedFires when a tag is added to a contact
manualTriggered manually via API
scheduledRuns hourly, daily, weekly, or monthly in the configured timezone

Actions

Actions are the steps executed when a workflow is triggered. Chain multiple actions together with conditions and delays.

send_emailSend email via Resend
ai_researchResearch and enrich a contact with structured results
add_tag / remove_tagModify contact tags
waitDelay execution by a specified duration
conditionIf/else branching based on contact data
webhookHTTP request to an external URL
update_contactModify contact fields

Webhook Events

A webhook step sends the configured method, headers, and body to an external URL. The method defaults to POST; the body defaults to the workflow context below, but you can replace it and interpolate workflow values.

Webhook Payloadjson
{
  "projectId": "proj_123",
  "contactId": "ct_m1n2o3",
  "contactEmail": "jane@example.com",
  "formResponseId": "resp_a1b2c3d4",
  "metadata": {}
}

MCP Server

LinkyCal ships a built-in Model Context Protocol server, so AI agents can check availability, book meetings, manage contacts, and inspect forms on your behalf. It speaks Streamable HTTP at a single endpoint:

MCP Endpointtext
https://linkycal.com/api/mcp

MCP connections use OAuth 2.1. During browser authorization you choose one eligible project and approve read/write access. Every tool is hard-scoped to that grant, so agents never pass a project ID and cannot reach another project.

Connecting

Register the endpoint below in your MCP client. LinkyCal opens in your browser so you can sign in, choose a project, review access, and approve the connection.

MCP Client Configjson
{
  "mcpServers": {
    "linkycal": {
      "type": "http",
      "url": "https://linkycal.com/api/mcp"
    }
  }
}
Review connected clients under MCP & APIs. Revoking a connection invalidates its access and refresh tokens. MCP rejects LinkyCal REST API keys at this boundary.
Once connected, just ask in natural language: “Book a 30-minute demo with Sarah Chen next Tuesday at 2pm” — the agent picks the right tools, checks availability, and confirms the booking.

Available Tools

The server exposes 92 tools for the project, grouped by read or write access, then by domain. Read tools return JSON; write tools enforce the same plan limits and validation as the dashboard. create_form always creates a page form and ignores type. Each step is one page.

Read tools

Discover IDs and inspect project state. Read tools never mutate LinkyCal data.

Projectget_project · get_project_entitlements · get_custom_css
Bookingslist_bookings · get_booking · get_available_slots · get_booking_form_response
Event typeslist_event_types · get_event_type
Calendarslist_project_calendars · get_event_type_calendars
Scheduleslist_schedules · get_schedule
Contactslist_contacts · get_contact · get_contact_activity
CRMlist_contact_views
Tagslist_contact_tags · get_contact_tag
Formslist_forms · get_form · list_form_responses · get_form_response · get_form_response_file
Workflowslist_workflows · get_workflow · list_workflow_runs · get_workflow_run
Activitylist_recent_activity
Analyticsget_analytics_filters · get_analytics_overview · get_booking_funnel_analytics · get_form_funnel_analytics · list_analytics_integrations

Write tools

Create or change project data. Read the current resource first; destructive and externally visible operations are identified by MCP annotations.

Projectupdate_project · set_custom_css · delete_custom_css · upload_project_asset · delete_project_asset
Bookingscreate_booking · cancel_booking · confirm_booking · decline_booking
Event typescreate_event_type · update_event_type · delete_event_type
Calendarsupdate_event_type_calendars
Schedulescreate_schedule · update_schedule · delete_schedule · set_schedule_rules · add_schedule_override · delete_schedule_override
Contactscreate_contact · update_contact · set_contact_next_action · complete_contact_next_action · delete_contact
Tagscreate_contact_tag · update_contact_tag · delete_contact_tag · add_tag_to_contact · remove_tag_from_contact
CRMcreate_contact_view · update_contact_view · delete_contact_view · import_contacts · set_contact_stage · enrich_contact · seed_contact_pipeline
Formscreate_form · update_form · delete_form · create_form_step · update_form_step · delete_form_step · reorder_form_steps · create_form_field · update_form_field · delete_form_field · reorder_form_fields · delete_form_response
Workflowscreate_workflow · update_workflow · delete_workflow · create_workflow_step · update_workflow_step · delete_workflow_step · reorder_workflow_steps · trigger_workflow · test_workflow
Analyticsconfigure_analytics_integration
Analytics tools use the same project-scoped aggregate actions as REST. Pass an event type ID or form ID for exact stages; common inputs support preset/custom dates, UTM filters, direct/widget source, and device type. Detailed reports and provider configuration require Pro or Business.

get_booking_funnel_analytics and get_form_funnel_analytics return unique-journey stage visitors, continuation, drop-offs, skips, safe context distributions, and the detailed-data boundary. Aggregate outputs exclude names, emails, raw answers, journey IDs, IP addresses, and raw errors. configure_analytics_integration accepts only GA4, Meta Pixel, or PostHog public identifiers and an allowlisted US/EU PostHog host.

Workflows are read-only over MCP by design — agents can inspect automations but not modify them. Booking writes still trigger your workflows, emails, and calendar sync exactly like the API.

API Keys

Create REST API keys in the dashboard under MCP & APIs. Include your key in the Authorization header as a Bearer token for protected /api/projects/:projectId/* requests. MCP uses OAuth instead; visitor endpoints are anonymous and must not receive this header.

Authorization Headerbash
curl -H "Authorization: Bearer lc_live_a1b2c3d4e5f6..." \
  "https://linkycal.com/api/projects/proj_123/contacts"
API keys are project-scoped management credentials. Keep them in server-side secret storage or a local agent configuration. Never expose them in visitor-side code.

API and MCP access are available on every plan and share the workspace's monthly integration-request quota. Exhausted quotas return plan_usage_limit_reached even when the key itself is valid. Read GET /api/projects/:projectId/entitlements when your integration needs the current resource limits.

Send either a dashboard session or an API key, never both. If a request has a valid session plus any Authorization header, LinkyCal returns ambiguous_credentials. An invalid Bearer value returns invalid_api_key and never falls back to a session. A key used with another project returns api_key_project_mismatch; a project without current API entitlement returns api_access_unavailable.

Rate Limits

The API enforces per-IP rate limits to ensure fair usage. If you exceed the limit, you'll receive a 429 Too Many Requests response.

EndpointLimit
Availability checks60 requests/minute per IP
Booking creation10 requests/minute per IP
Form responses30 requests/minute per IP
Form response uploads30 requests/minute per IP
Form step submissions60 requests/minute per IP
These per-IP limits apply to visitor endpoints. Protected project requests are additionally constrained by project ownership, current plan, and resource limits.

Error Handling

All errors return a JSON object with a descriptive error field. Use the HTTP status code to determine the type of error.

Error Responsejson
{
  "error": "Descriptive error message"
}

Common status codes:

400Validation error — check your request body or parameters
401Unauthorized — missing or invalid API key
403Forbidden — plan limit reached or feature not available
404Not found — resource does not exist
409Conflict — a unique value already exists or a referenced resource cannot be deleted
429Rate limited — too many requests, try again later
500Server error — something went wrong on our end

Pagination

Pagination is endpoint-specific. Do not assume that every list uses the same model.

Contacts

Offset pagination with limit and offset. The default limit is 50, the maximum is 100, and the response includes the full filtered total.

Contact activity

Cursor pagination with a default limit of 20 and maximum of 100. Pass the returned opaque nextCursor unchanged.

Tags

Optional cursor pagination from 1 to 100 items. When using cursor, repeat the same limit on the next request.

Other lists

Other list operations currently return their complete result unless their section or OpenAPI operation declares a limit.

Webhooks

Configure webhooks via workflow actions to receive real-time notifications about events in your project. Create a workflow with a trigger (e.g. form_submitted) and add a webhook action pointing to your URL.

Example: Workflow with Webhookjson
{
  "trigger": "booking_created",
  "actions": [
    {
      "type": "webhook",
      "config": {
        "url": "https://your-app.com/webhooks/linkycal",
        "method": "POST",
        "headers": {
          "X-Webhook-Secret": "your_secret"
        }
      }
    }
  ]
}
Always verify webhook payloads using a shared secret to ensure requests originate from LinkyCal.

Ready to integrate?

Build visitor forms and booking flows without a secret, then use a project key for trusted management integrations.