API referenceJob series

Job series

Define recurring jobs that generate instances on a schedule.

Use this page to create and manage recurring job schedules through the Zoop API. It covers all four job_series tools, a complete working example, and a reference for errors.

A job series is a recurring-job template. You define the schedule once and Zoop generates the individual job instances automatically. Think of the series as the blueprint and the generated jobs as the copies.

  • Series template — the schedule, title, line items, and team assignments. Managed with job_series.* tools.
  • Job instances — the individual jobs Zoop generates from the template. Managed with jobs.* tools.

When you update a series, Zoop regenerates all future instances from the updated template. When you end a series, it stops generating new instances and cancels the pending ones.

For the end-user view of how recurring jobs work in the product, see recurring jobs.

Scopes

Scopes control what your API token is allowed to do. You request them when you create an API key in Zoop under Settings > API keys.

ScopeGrants
read:job_seriesjob_series.list, job_series.get
write:job_seriesjob_series.create, job_series.update, job_series.end

write:job_series does not include read:job_series. Grant both if your integration needs to read and write.

Who can create and update a series

job_series.create and job_series.update require a user-bound token — an API key tied to a real Zoop team member (zoop_uk_ prefix). These tokens carry an identity, which is required to author a series.

Tenant-key tokens (zoop_tk_ prefix) have no user identity and will receive an invalid_input error on those two calls.

job_series.end has no such restriction — either token type can end a series.

If you need to write series from an automated pipeline (no human in the loop), create a dedicated service-account team member in Zoop and issue a zoop_uk_ key for that account. See API keys.

Tools

job_series.list

Lists recurring job series for the tenant, most recent first. Returns page-based results (default page size: 20).

Scope: read:job_series

path
parameter

Filter by customer. Returns only series linked to this customer.

path
parameter

1-based page number. Defaults to 1.

Returns: { data, count, page, limit }

Each row in data is the series template record. To fetch the generated job instances, use jobs.list with the seriesId filter.


job_series.get

Fetches a single series by ID, including its template line items and assignments.

Scope: read:job_series

path
parameter

The series ID.

Returns the full series record with job_series_line_items and job_series_assignments embedded. Returns not_found if the series does not exist within your tenant.


job_series.create

Creates a recurring job series. After saving, Zoop immediately tries to generate the first batch of job instances so they are ready to use.

Scope: write:job_series — requires a user-bound token (zoop_uk_).

path
parameter

Series title, max 200 characters.

path
parameter

The recurrence rule string (RFC 5545 iCalendar format) that defines how often jobs repeat. FREQ must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. Sub-daily frequencies (BYHOUR, BYMINUTE, BYSECOND) are not allowed. If you include COUNT, it must be between 1 and 366. If you include UNTIL, it must be within 5 years of today.

Example: FREQ=WEEKLY;BYDAY=MO — generates a job every Monday.

You can use a tool like rrule.js playground to build and test rules before passing them here.

path
parameter

IANA timezone name for the schedule — for example, America/Los_Angeles or America/New_York. This controls what "Monday" means: a job scheduled on Monday Pacific time fires at midnight Pacific, not UTC. Invalid timezone strings are rejected.

See the IANA tz database for valid names.

path
parameter

The date the series starts, in YYYY-MM-DD format (for example, 2026-07-07). This anchors the recurrence rule — it is the first possible date a job instance can be generated on. Cannot be changed after the series is created.

path
parameter

Link the series to a customer. Must belong to your tenant.

path
parameter

Free-text description, max 5000 characters.

path
parameter

A fixed price for each generated job, overriding any line-item total. Non-negative.

path
parameter

The date the series ends. No instances are generated after this date. Omit for an open-ended series.

path
parameter

Cap the total number of instances the series generates. Must be between 1 and 366. Omit for no cap.

path
parameter

Template line items copied onto each generated job. Defaults to an empty array.

path
parameter

Team member IDs to assign to each generated job. Every ID must be a member of your tenant. Defaults to an empty array.

Returns the created series record. Zoop generates the first window of job instances synchronously — they are usually available via jobs.list immediately after a successful call. If that first generation step fails (for example, a transient database error), the series is still created and a background job will generate the instances shortly after.


job_series.update

Updates a recurring job series. This is an edit-all operation: changes apply to the whole series template, not just future occurrences.

Scope: write:job_series — requires a user-bound token (zoop_uk_).

After a successful update, Zoop cancels all future unscheduled and scheduled job instances and regenerates them from the updated template. Jobs with status done or cancelled are left intact — they are the historical record and cannot be regenerated.

dtstart cannot be changed after a series is created. Passing it to update returns an invalid_input error. To use a different start date, end the existing series and create a new one.

path
parameter

The series ID.

path
parameter

Updated title, max 200 characters.

path
parameter

New recurrence rule. Same constraints as job_series.create.

path
parameter

New IANA timezone name.

path
parameter

Change or clear the customer link.

path
parameter

Updated description, max 5000 characters.

path
parameter

Updated fixed price, or null to clear it.

path
parameter

New end date, or null to remove it.

path
parameter

New occurrence cap, or null to remove it.

path
parameter

If provided, replaces the template line items wholesale — the existing list is deleted and the new list is inserted. Omit to leave line items unchanged.

path
parameter

If provided, replaces the assignment list wholesale. Omit to leave assignments unchanged.

Returns the updated series record on success. Returns not_found if the series does not exist within your tenant.

After a successful update, Zoop wipes future unscheduled and scheduled instances synchronously, then attempts to regenerate the window. If regeneration fails (transient error), the series template is still updated and a background cron will regenerate the instances.


job_series.end

Permanently stops a recurring job series. There is no archive or soft-delete — once ended, a series cannot be resumed.

Scope: write:job_series

Ending a series is one-way. If you only want to pause generation temporarily, use job_series.update to set ends_on to a future date instead — you can move it again later.

Ending a series does two things in sequence:

  1. Sets ends_on to today, so no further instances are generated.
  2. Cancels every future unscheduled and scheduled instance that has a scheduled_start from now onward.

Jobs with status done or cancelled are untouched — completed work is preserved.

path
parameter

The series ID.

Returns { ended: true, id } on success. Returns not_found if the series does not exist within your tenant.


Example: create a weekly recurring job

The example below creates a series that generates a job every Monday starting 2026-07-07, in the Pacific timezone, assigned to one team member. Swap in your real API key, customer ID, and team member ID before running it.

curl -X POST https://app.zoop.example/api/mcp \
  -H "Authorization: Bearer zoop_uk_<your-secret>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "job_series.create",
      "arguments": {
        "title": "Weekly lawn maintenance",
        "rrule": "FREQ=WEEKLY;BYDAY=MO",
        "timezone": "America/Los_Angeles",
        "dtstart": "2026-07-07",
        "customer_id": "aaaaaaaa-0000-0000-0000-000000000001",
        "line_items": [
          {
            "description": "Mow and edge",
            "unit_price": 80.00,
            "quantity": 1
          }
        ],
        "assigned_user_ids": ["bbbbbbbb-0000-0000-0000-000000000002"]
      }
    }
  }'

A successful response looks like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{"id":"cccccccc-0000-0000-0000-000000000003","tenant_id":"...","title":"Weekly lawn maintenance","rrule":"FREQ=WEEKLY;BYDAY=MO","timezone":"America/Los_Angeles","dtstart":"2026-07-07","ends_on":null,"max_occurrences":null,"flat_price":null,"customer_id":"aaaaaaaa-0000-0000-0000-000000000001","created_at":"2026-06-13T00:00:00Z"}"
      }
    ]
  }
}

To list the generated job instances after creating the series:

const instancesResponse = await fetch('https://app.zoop.example/api/mcp', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.ZOOP_API_KEY}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json, text/event-stream',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 2,
    method: 'tools/call',
    params: {
      name: 'jobs.list',
      arguments: {
        seriesId: 'cccccccc-0000-0000-0000-000000000003',
      },
    },
  }),
})

Key concepts

These are the common points of confusion when first working with this API.

Series vs. instances

The job_series tools manage the template — the schedule definition, title, line items, and assignments. The generated jobs are regular jobs rows accessed through the jobs tools. Use jobs.list with the seriesId filter to retrieve all instances for a series.

dtstart is immutable

Once a series is created, dtstart cannot be changed. If you need a different start date, end the existing series and create a new one.

Update semantics — edit-all, not "this and future"

job_series.update applies every change to the whole series template, then regenerates all future instances. This differs from the Zoop product UI, which lets users choose "this and future." The API only supports edit-all.

When you update line_items or assigned_user_ids, the new value replaces the entire current list. Omit those fields to leave them unchanged.

Ending a series vs. pausing it

job_series.end is one-way. It caps ends_on at today and cancels all pending instances.

If you only want to pause generation temporarily, use job_series.update to set ends_on to a future date instead — jobs still generate up to that date, and you can move the date again later.


Error reference

Error kindMeaning
not_foundThe series ID does not exist in this tenant.
invalid_inputA required field is missing; the rrule is malformed; the timezone is not a recognized IANA name; dtstart was passed to update; the token is a tenant key (zoop_tk_) on create or update; or a referenced customer_id, catalog_item_id, or user ID does not belong to this tenant.
insufficient_scopeThe token does not have read:job_series or write:job_series as required.
conflictA conflicting series already exists (unique constraint).
internalAn unexpected server error — details are not returned to the client.

See errors for the full error reference and HTTP status codes.


Paginating through all series

job_series.list returns up to 20 series per call. To fetch more, pass page (starting at 1) to step through results. The response tells you how many total series exist (count), which page you are on (page), and the page size (limit, always 20).

let page = 1
let allSeries: unknown[] = []

while (true) {
  const response = await fetch('https://app.zoop.example/api/mcp', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.ZOOP_API_KEY}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json, text/event-stream',
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: page,
      method: 'tools/call',
      params: {
        name: 'job_series.list',
        arguments: { page },
      },
    }),
  })

  const envelope = await response.json()
  const text = envelope.result?.content?.[0]?.text
  const result = JSON.parse(text)

  allSeries = allSeries.concat(result.data)

  const totalPages = Math.ceil(result.count / result.limit)
  if (page >= totalPages) break
  page++
}

  • Jobs — fetch and manage the job instances generated by a series
  • Recurring jobs — end-user guide for recurring jobs in the product
  • Scopes — full scope catalog
  • API keys — how to get and use API keys
  • MCP server — transport, conventions, and the full tool catalog
  • Errors — error kinds and HTTP status codes