> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Zapier

> Build a private Zapier integration for Xquik with API-key auth, REST Hooks, extraction jobs, monitors, and X actions

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Use Xquik in Zapier when a workflow needs X (Twitter) search, account lookups, trends, publishing, extraction jobs, monitors, or webhook automation without an X Developer app.

Start with a private Zapier Platform CLI integration. Keep the first release focused on high-volume workflow primitives, then expand once real Zaps show which endpoints users need.

## Prerequisites

* [Xquik API key](/quickstart)
* Zapier account with Platform CLI access
* Node.js 18+
* HTTPS callback URLs for REST Hook testing

Install and sign in to the Zapier CLI:

```bash theme={null}
npm install -g zapier-platform-cli
zapier login
zapier init xquik-zapier --template minimal
cd xquik-zapier
npm install
```

## Integration Shape

<CardGroup cols={2}>
  <Card title="Auth" icon="key-round">
    API key field named `apiKey`, injected as `x-api-key`.
  </Card>

  <Card title="Base URL" icon="link">
    `https://xquik.com/api/v1`
  </Card>

  <Card title="Request Helper" icon="workflow">
    JSON requests, structured Xquik errors, and `Retry-After` handling.
  </Card>

  <Card title="Actions" icon="play">
    Search Tweets, Get Tweet, Get User, Get Trends, Create Tweet, Create Reply,
    Create Extraction, Create Monitor, and Create Webhook.
  </Card>

  <Card title="Triggers" icon="radio">
    New Matching Tweet polling, Monitor Event instant trigger, Extraction
    Completed polling, and Webhook Delivery Failure polling.
  </Card>
</CardGroup>

This gives Zapier builders API-key auth, 127 REST operations, monitor webhooks, extraction jobs, and agent workflows. The visible Zapier X templates focus on polling a query and taking one write action.

## API Key Auth

Add a custom auth field and inject it into every Xquik request:

```javascript theme={null}
const BASE_URL = "https://xquik.com/api/v1";

function addApiKeyHeader(request, z, bundle) {
  request.headers = request.headers || {};
  request.headers["x-api-key"] = bundle.authData.apiKey;
  return request;
}

async function testAuth(z) {
  const response = await z.request({
    method: "GET",
    url: `${BASE_URL}/account`,
  });

  return response.data;
}

module.exports = {
  authentication: {
    type: "custom",
    fields: [{ key: "apiKey", label: "Xquik API Key", required: true }],
    test: testAuth,
  },
  beforeRequest: [addApiKeyHeader],
};
```

## Shared Error Handling

Normalize Xquik responses in one helper so every action reports the same remediation:

```javascript theme={null}
function xquikErrorMessage(status, data, headers) {
  if (status === 401) {
    return "Authentication failed. Check the Xquik API key.";
  }

  if (status === 402) {
    return "Subscription or credits required. Update billing in Xquik.";
  }

  if (status === 429) {
    const retryAfter = headers?.["retry-after"];
    return retryAfter
      ? `Rate limited. Retry after ${retryAfter} seconds.`
      : "Rate limited. Retry after the cooldown period.";
  }

  return data?.message || data?.error || "Xquik request failed.";
}

function throwForXquikError(z, response) {
  if (response.status < 400) {
    return;
  }

  throw new z.errors.Error(
    xquikErrorMessage(response.status, response.data, response.headers),
    "XquikError",
    response.status,
  );
}
```

Call `throwForXquikError(z, response)` after each `z.request`.

## Starter Actions

<CardGroup cols={2}>
  <Card title="Search Tweets" icon="search">
    Call `GET /x/tweets/search` with `q`; use `cursor` for page loops and keep `limit` on bounded resumes.
  </Card>

  <Card title="Get Tweet" icon="message-square">
    Call `GET /x/tweets/{id}` with a tweet ID.
  </Card>

  <Card title="Get User" icon="user">
    Call `GET /x/users/{id}` with a user ID.
  </Card>

  <Card title="Get Trends" icon="trending-up">
    Call `GET /x/trends` with optional `woeid` and `count`.
  </Card>

  <Card title="Create Tweet" icon="send">
    Call `POST /x/tweets` with account, text, and optional public media URLs.
  </Card>

  <Card title="Create Reply" icon="reply">
    Call `POST /x/tweets` with account, text, and `reply_to_tweet_id`.
  </Card>

  <Card title="Create Extraction" icon="database">
    Call `POST /extractions` with `toolType`, query fields, and result limit.
  </Card>

  <Card title="Create Monitor" icon="radio">
    Call `POST /monitors` with username and event types.
  </Card>

  <Card title="Create Webhook" icon="webhook">
    Call `POST /webhooks` with callback URL and event types.
  </Card>
</CardGroup>

## Result Handoff

Use Zapier samples and `outputFields` so later Zap steps map stable values. Return compact objects from actions and arrays from triggers; do not pass full API responses into Zap history, Slack messages, CRM rows, or retry queues. Use snake\_case storage keys for handoff rows even when direct API responses use camelCase.

<CardGroup cols={2}>
  <Card title="Search Tweets action" icon="search">
    Return tweet rows with `id`, `text`, `author__username`, `createdAt`, and optional `url`; store `tweet_id`, `author_username`, `created_at`, `has_next_page`, and `next_cursor` when a Zap loops pages.
  </Card>

  <Card title="User profile rows" icon="users">
    Return source `id` as `user_id`, plus `username`, `name`, `followers`, `verified`, `profile_picture`, `has_next_page`, `next_cursor`, and the lookup or search input.
  </Card>

  <Card title="Trend rows" icon="trending-up">
    Return each trend `name`, `rank`, `query`, and `description`. Keep response `count`, `woeid`, and the selected region with the Zap run.
  </Card>

  <Card title="Tweet or Reply write" icon="send">
    Send a unique `Idempotency-Key`. Store `id`, `status`, `billing`, `result`, and `statusUrl`. Poll while `terminal` is false. Retry only when `safeToRetry` is true, using a new key.
  </Card>

  <Card title="Media attachments" icon="image">
    For tweets or replies, pass public URLs in `media`; do not send `media_ids`. For DMs, upload first, pass 1 `media_id` in `media_ids`, store `message_id`, and leave `reply_to_message_id` unset.
  </Card>

  <Card title="Monitor and webhook setup" icon="radio">
    Return monitor `id`, `username`, `xUserId`, `eventTypes`, `isActive`, and `nextBillingAt`; return webhook `id`, `url`, `eventTypes`, and one-time `secret`. For Zap storage rows, map production `deliveryId` to `delivery_id` for receiver retry de-dupe and `streamEventId` to `stream_event_id` when one monitor event should process once across endpoint changes.
  </Card>

  <Card title="REST Hook trigger" icon="fingerprint">
    Return `id`, `deliveryId`, and `streamEventId`; choose `deliveryId` for endpoint retry de-dupe or `streamEventId` when one monitor event should process once across webhook changes.
  </Card>

  <Card title="Stored Event Replay" icon="activity">
    Call `GET /events` with `after` when a Zap needs replay. Map `id`, `monitorId`, `monitorType`, `occurredAt`, `hasMore`, and `nextCursor` to `event_id`, `monitor_id`, `monitor_type`, `occurred_at`, `has_more`, and `next_cursor`.
  </Card>

  <Card title="Receiver acceptance" icon="copy-check">
    Return `2xx` after accepting duplicate `deliveryId` or `streamEventId`; keep endpoint signing values, raw request body, raw signature, and full headers out of Zap history, tables, Slack messages, CRM rows, and retry queues.
  </Card>

  <Card title="Extraction polling trigger" icon="database">
    Return completed job `id`, `toolType`, and `status`; store `extraction_id`, `tool_type`, `status`, `has_more`, and `next_cursor` before batch Zaps fetch detail rows.
  </Card>
</CardGroup>

Use a Zapier dropdown to map friendly region names to WOEID values before calling Xquik.

Example search action:

```javascript theme={null}
async function performSearchTweets(z, bundle) {
  const response = await z.request({
    method: "GET",
    url: `${BASE_URL}/x/tweets/search`,
    params: {
      q: bundle.inputData.q,
      limit: bundle.inputData.limit || 25,
    },
  });

  throwForXquikError(z, response);
  return response.data.tweets || [];
}

module.exports = {
  key: "search_tweets",
  noun: "Tweet",
  display: {
    label: "Search Tweets",
    description: "Find recent tweets that match a search query.",
  },
  operation: {
    inputFields: [
      { key: "q", label: "Query", required: true, type: "string" },
      { key: "limit", label: "Limit", required: false, type: "integer" },
    ],
    perform: performSearchTweets,
    sample: {
      id: "1840000000000000000",
      text: "Example tweet text",
      author: { username: "example_user" },
      url: "https://x.com/example_user/status/1840000000000000000",
    },
    outputFields: [
      { key: "id", label: "Tweet ID" },
      { key: "text", label: "Text" },
      { key: "author__username", label: "Author Username" },
      { key: "url", label: "URL" },
    ],
  },
};
```

## Trigger 1: New Matching Tweet

Use polling for query-based alerts. Return a stable array of tweet records and let Zapier deduplicate by `id`.

```javascript theme={null}
async function performNewMatchingTweet(z, bundle) {
  const response = await z.request({
    method: "GET",
    url: `${BASE_URL}/x/tweets/search`,
    params: {
      q: bundle.inputData.q,
      limit: 25,
    },
  });

  throwForXquikError(z, response);
  return response.data.tweets || [];
}
```

## Trigger 2: Monitor Event REST Hook

Use REST Hooks for monitor events so Zapier receives new tweets, replies, quotes, and retweets immediately.

### Subscribe

```javascript theme={null}
async function subscribeHook(z, bundle) {
  const response = await z.request({
    method: "POST",
    url: `${BASE_URL}/webhooks`,
    body: {
      url: bundle.targetUrl,
      eventTypes: bundle.inputData.eventTypes,
    },
  });

  throwForXquikError(z, response);
  return { id: response.data.id };
}
```

### Unsubscribe

```javascript theme={null}
async function unsubscribeHook(z, bundle) {
  const response = await z.request({
    method: "DELETE",
    url: `${BASE_URL}/webhooks/${bundle.subscribeData.id}`,
  });

  throwForXquikError(z, response);
  return response.data;
}
```

### Perform

```javascript theme={null}
function performMonitorEvent(z, bundle) {
  const payload = bundle.cleanedRequest;

  return [
    {
      id: payload.streamEventId || payload.deliveryId,
      deliveryId: payload.deliveryId,
      eventType: payload.eventType,
      occurredAt: payload.occurredAt,
      username: payload.username,
      tweetId: payload.data?.id,
      text: payload.data?.text,
      authorUserName: payload.data?.author?.userName || payload.username,
    },
  ];
}
```

Use `performList` to return one recent monitor event with the same schema. Zapier requires sample output fields that match live webhook payloads.

## Trigger 3: Extraction Completed

Use polling when users want bulk jobs without webhook setup:

```javascript theme={null}
async function performCompletedExtractions(z) {
  const response = await z.request({
    method: "GET",
    url: `${BASE_URL}/extractions`,
    params: { status: "completed", limit: 25 },
  });

  throwForXquikError(z, response);
  return response.data.extractions || [];
}
```

## Trigger 4: Webhook Delivery Failure

Use polling for operations teams that need delivery alerts:

```javascript theme={null}
async function performWebhookFailures(z, bundle) {
  const response = await z.request({
    method: "GET",
    url: `${BASE_URL}/webhooks/${bundle.inputData.webhookId}/deliveries`,
  });

  throwForXquikError(z, response);
  return (response.data.deliveries || []).filter(
    (delivery) => delivery.status === "failed",
  );
}
```

## Test Coverage

Add focused Zapier tests before sharing the private app:

<CardGroup cols={2}>
  <Card title="Auth Header Injection" icon="shield-check">
    Every request includes `x-api-key` from `bundle.authData.apiKey`.
  </Card>

  <Card title="Invalid Key" icon="key-round">
    `401` returns "Authentication failed. Check the Xquik API key."
  </Card>

  <Card title="Rate Limit" icon="timer">
    `429` includes `Retry-After` in the user-facing message when present.
  </Card>

  <Card title="REST Hook Subscribe" icon="webhook">
    `POST /webhooks` sends `bundle.targetUrl` and selected event types.
  </Card>

  <Card title="REST Hook Unsubscribe" icon="radio">
    `DELETE /webhooks/{id}` uses `bundle.subscribeData.id`.
  </Card>

  <Card title="Sample Output" icon="database">
    Trigger samples include `id`, `eventType`, `occurredAt`, tweet text, and author username.
  </Card>

  <Card title="Search Action" icon="search">
    Search returns an array of tweets with stable IDs.
  </Card>
</CardGroup>

Run the local Zapier suite with:

```bash theme={null}
zapier-platform test
zapier-platform validate
```

## Launch Checklist

* Keep the first version private until auth, REST Hook lifecycle, sample output, and rate-limit behavior are verified.
* Use clear labels in the Zap editor: Search Tweets, Create Tweet, Monitor Event, Extraction Completed.
* Document which actions consume credits before publishing to a broader team.
* Add more endpoints only when Zap history shows repeated manual API Request steps.

## Next Steps

* Read [API Reference](/api-reference/overview) for auth, rate limits, and errors.
* Read [Webhooks](/webhooks/overview) for payload shape and retries.
* Read [Extraction Workflow](/guides/extraction-workflow) for job creation and result pagination.
* Use [n8n](/guides/n8n) when the team prefers visual workflows without a custom Zapier app.
