openapi: 3.2.0 info: title: Skai Bulk Update API description: "# Overview\nSkai APIs provide programmatic access to advertising data and campaign management across Search, Social, and Retail Media publishers.\n\n## Choosing the Right API\n\n| What you want to do | API to use | Scale | Notes |\n|---|---|---|---|\n| Pull performance data, metrics, or any reportable field | [Reporting](#tag/Synchronous-Reports) or [Async Reporting](#tag/Asynchronous-Reports) | Unlimited | Primary data access API — the main Skai value-prop |\n| Discover what columns and metrics are available | [Available Columns](#operation/getAvailableColumns) | — | Full list of reportable fields per entity type |\n| Create or update campaigns, keywords, bids, budgets, targeting, and more — at scale | [Bulk Update (One File)](#tag/Bulk-Update) | Millions of rows | Supports Skai's main entity types and hundreds of attributes; all publishers except Meta |\n| Create/update a small number of campaigns or ad groups (common attributes only) | [Campaigns](#tag/Campaigns) / [Ad Groups](#tag/Ad-Groups) / [Ads](#tag/Ads) | Thousands | Limited attribute set — use Bulk Update for full control |\n| Manage Meta (Facebook/Instagram) entities | [Meta Campaigns](#tag/Meta-Campaigns) / [Meta Ad Groups](#tag/Meta-Ad-Groups) / [Meta Ads](#tag/Meta-Ads) | Thousands | Meta-specific tag and attribution management |\n| Use Skai from an AI coding assistant (Claude, Cursor, ChatGPT, Windsurf) | [MCP Integration](#tag/MCP) | — | Full reporting access via natural language |\n\nSkai APIs are RESTful and language agnostic. Authentication uses Bearer tokens over HTTPS.\n\n## What Data Can I Access?\n\nSkai aggregates advertising data across three publisher categories:\n\n| Publisher category | Examples |\n|---|---|\n| **Search** | Google Ads, Microsoft Ads, Yahoo Japan, Baidu, and others |\n| **Social (excl. Meta)** | Pinterest, Snapchat, TikTok, LinkedIn, Reddit, and others |\n| **Social (Meta)** | Facebook, Instagram |\n| **Retail Media** | Amazon Ads, Walmart, Instacart, Kroger, Target, and 100+ others |\n\n**Reportable entity types:**\n\n| Entity | Description | Publishers |\n|---|---|---|\n| `CAMPAIGN` | Campaign-level data | All |\n| `ADGROUP` | Ad group / ad set level | All |\n| `KEYWORD` | Keyword-level performance and settings | Search, Retail Media |\n| `AD` | Individual ad creatives | All |\n| `PRODUCT_ASSET` | Product-level data for shopping and retail media (called \"Products\" in the Skai UI) | Retail Media, Search Shopping |\n| `PRODUCT_TARGETING` | Product targeting entities — ASINs, categories, and product attributes | Retail Media |\n| `PORTFOLIO` | Portfolio-level budget aggregations and pacing | All |\n\n**Available metric categories per entity:**\n\n- **Performance** — Impressions, Clicks, Cost, Conversions, Revenue, ROAS, CTR, CPC, and more\n- **Attributes** — Names, statuses, budgets, bids, targeting settings, and publisher-specific fields\n- **Account-configured** — Dimensions (custom tagging labels), Conversion events (publisher, pixel, and 3rd-party), Custom Metrics (formula-based calculations your team defines)\n\nUse [Available Columns](#operation/getAvailableColumns) to see the complete column list for any entity — including full descriptions and types. A static reference is embedded in that endpoint's documentation.\n\n\n## Authentication\nThe Skai API uses the Bearer authentication scheme.\nThe first step is to generate a *refresh token* (once), which you can then exchange for a temporary *access token*, programmatically, before making an API call.\n\n> Note: The user you use to generate your *refresh token* will determine the token's permissions. API access is allowed for users with Standard role or higher.\nIt is recommended that you create and use a specialized user for your API requests.\n\n\n#### Step 1: Get a Refresh Token\nYou only need to do this once, for each API user you plan to use. \n\nLog into [this page](https://login.kenshoo.com/api/dev/refresh-token) in order to get your *refresh token* and *client ID*. The user you log in with will be the user accessing the API. \nPlease store your refresh token in a secure place. While it is not possible to recover a refresh token, you can generate a new one. The refresh token does not expire.\n\n\n#### Step 2: Generating an Access Token\nBefore making API calls, your code uses the permanent *refresh token* to generate a temporary *access token*.\n\nMake a call to /api/v1/token (as shown below) with your *refresh token* and *client ID* to generate an *access token*:\n\n curl -X POST -d \"refresh_token=&client_id=\" \\\n https://services.kenshoo.com/api/v1/token\n\nNote: the client_id and refresh token should be sent in the POST request body, as the refresh token is confidential and should not be sent as url param.\nthe API will reject refresh tokens sent in url params.\n\nGet token for specific agency context:\nIn case your API user is assigned to multi accounts (agencies), you should explicitly specify in the get access-token request which agency context you would like to receive the token for.\nJust add to the request mentioned above another form param called *agency_id*, and pass the relevant agency ID like this:\n \n curl -X POST -d \"refresh_token=&client_id=&agency_id=\" \\\n https://services.kenshoo.com/api/v1/token\n\nToken expiration:\nPlease check for token expiration before sending another API request , you have 2 options:\n\n1. Call the API and get 401 status code indicating authentication failed.\n2. Consider the *expires_in* field of the token to issue a new access token.\n\nThe response will return a JSON containing the token and time for expiration in seconds.\nIt is recommended to use the token expiration time and reuse tokens while they are still valid, to prevent rate limit issues with generating new tokens too often.\n\n {\"email\":\"my.user@skai.io\",\"expires_in\":21600,\"access_token\":\"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGxvbWkuY29oZW5Ac2thaS5pbyIsImV4cCI6MTcxNDU2NjgxMSwiaXNzIjoiaHR0cDovL2tlbnNob28uY29tL2xvZ2luLXNlcnZlciIsInVzZXJpZCI6MzU5NDMsImFnZW5jeUlkIjoxNSwibmFtZSI6IlNobG9taSBDb2hlbiIsInJvbGVzIjpbIktlbnNob28gQWRtaW4iLCJTa2FpIERldmVsb3BlciJdLCJhZ2VuY3lfcm9sZXMiOlt7ImFnZW5jeUlkIjoxNSwicm9sZSI6IktlbnNob28gQWRtaW4ifV0sImJpbGxpbmdJZCI6OTIwMzEsImFwaWMiOiI5MjAzMSIsIm9yaSI6ImFwaSIsImFsbG93ZWRfYXBwcyI6W119.R3tHoaecUrMGzijnF5suo9SVsffXWbWxdMv5fdB3Jx8\"}\n\n\n\n\n\n#### Step 3: Making an API call\nWith any API call to all Skai APIs, you must send a valid *access token* in the Authorization header when making requests. For example:\n\n curl -H \"Authorization: Bearer \" -X POST \\\n https://services.kenshoo.com/api/v1/campaigns\n\n\n## Rate Limits\nAPI calls are limited per user, to the following:\n - 60 requests per minute\n - 2,000 requests per hour\n\nWhen you meet the limit, you receive the following 429 HTTP error: “API rate limit exceeded”.\nWhen calling any API endpoint the response headers will show the limits relevant to this user, and the number of remaining calls you can make within the current minute/hour.\n\n\n## Reporting Best Practices\n\n- **Filter for non-zero data:** For performance reports, filter to rows where a key metric (e.g., impressions > 0) to reduce report size and speed up generation.\n- **Scope structure reports:** Apply a filter like \"Last updated > X days ago\" to retrieve only recently changed entities.\n- **Use Async for large datasets:** If your report may return more than a few thousand rows, use [Async Analysis Reports](#tag/Asynchronous-Reports) and poll for results rather than the synchronous endpoint.\n\n\n## Group by and Segmentation\n### Understanding Group by and Segmentation\nWhen querying the /api/v1/reports/async/analysis and /reports endpoints, the breakdown_type parameter\ndetermines how data is structured.\n- FLAT: Returns unsegmented data without any grouping.\n- GROUP: Allows data segmentation based on specified columns (e.g., by date).\n- SEGMENT: Enables segmentation by date and an additional column, such as CampaignId.\n\n### How Group by works\nWhen using \"breakdown_type\": \"GROUP\", the group_bys parameter defines how the data is grouped. For instance:\n\"group_bys\": [ { \"name\": \"Day\", \"group\": \"TimeSegment\" } ]\n This groups data only by date, meaning campaign details won’t be included, similar to what is displayed in the grid export.\n\n| Conv. | Cost | Day |\n|-------|------|------------|\n| 2 | 100 | 09/29/2024 |\n| 3 | 200 | 09/28/2024 |\n\n### Using SEGMENT for Additional Grouping\nTo segment data by both date and another column (e.g., CampaignId), use \"breakdown_type\": \"SEGMENT\", specifying only the date column under group_bys while including the additional column in fields. Example:\n\"breakdown_type\": \"SEGMENT\",\n\"group_bys\": [ { \"name\": \"Day\", \"group\": \"TimeSegment\" } ],\n\"fields\": [ { \"name\": \"CampaignId\", \"group\": \"ATTRIBUTES\" } ]\n\nThis ensures data is segmented by day while preserving campaign details.\n\n| Campaign ID | Conv. | Cost | Day |\n|-------------|-------|------|------------|\n| 25000 | 1 | 50 | 09/29/2024 |\n| 25001 | 1 | 50 | 09/29/2024 |\n| 25000 | 2 | 150 | 09/28/2024 |\n| 25001 | 1 | 50 | 09/28/2024 |\n" version: 1.0.0 x-logo: url: https://grid.kenshoo.com/resources-frontend/latest/kenshoo_logo/skai-logo-devportal.svg backgroundColor: '#FFFFFF' altText: Skai servers: - url: https://services.kenshoo.com security: - BearerAuth: [] tags: - name: Bulk Update description: '
Publishers: Search Social (excl. Meta) Retail Media
' paths: /api/v1/bulk_update: post: tags: - Bulk Update summary: Make bulk changes to multiple entities description: "Upload a CSV bulksheet to create and update campaigns, ad groups, keywords, ads, and more — across dozens of publishers — in a single API call.\n\n#### Unmatched publisher breadth\n\nNo other platform offers programmatic create and edit operations across this many publishers in one API. Skai's Bulk Update covers:\n\n| Category | Publishers |\n|---|---|\n| Search | Google Ads, Microsoft Ads, Yahoo Japan, Baidu, and others |\n| Retail Media | Amazon (SP/SB/SD), Walmart (SP/SB/SV), Instacart, Criteo, DoorDash, Sam's Club, Bol, Topsort, Moloco, Koddi, and 100+ others |\n| Social | Pinterest, Snapchat, TikTok, Apple Search Ads |\n| Social (Meta, LinkedIn, Reddit) | Dimension assignment and basic status edits |\n\n**Supported operations by entity type:**\n\n| Entity | Create | Edit |\n|---|---|---|\n| Campaign | Yes | Yes |\n| Ad Group | Yes | Yes |\n| Keyword | Yes | Yes |\n| Ad | Yes | Yes |\n| Product Group | — | Yes (dimension assignment) |\n| Target | — | Yes (dimension assignment) |\n| Search Term | — | Yes (dimension assignment) |\n\nThe file format is identical to what Skai users export and import manually in the platform — every attribute visible in the Skai UI is editable through this API.\n\n---\n\n#### The bulksheet format\n\nBulksheets are UTF-8 CSV files. Each row is one entity. **Use `bulk_update_type=MULTI_ENTITY_FILE` (recommended)** — it allows any mix of entity types in a single file using an `Entity Type` column as the first column:\n\n```\nEntity Type,Profile ID,Campaign ID,Ad Group ID,Campaign Name,Ad Group Name,Keyword,Keyword Match Type,Keyword Bid,Status\nCampaign,55,,,,,,,,Active\nAd group,55,1001,,,Brand Ad Group,,,, Active\nKeyword,55,1001,5001,,,running shoes,exact,0.50,Active\n```\n\nIf you need single-entity files, set `bulk_update_type` to the specific entity (e.g. `CAMPAIGN`) and omit the `Entity Type` column — though everything supported in single-entity mode is also supported in `MULTI_ENTITY_FILE`.\n\n---\n\n#### Editing entities\n\nTo edit, each row needs only three things:\n1. **Entity Type** — `Campaign`, `Ad group`, `Ad`, or `Keyword`\n2. **The entity's Skai ID** — `Campaign ID`, `Ad Group ID`, `Ad ID`, or `Keyword ID`\n3. **Only the columns you want to change** — all other columns can be omitted\n\n> **Skai IDs vs publisher IDs:** Bulksheets use Skai's internal IDs, not publisher IDs. Retrieve them via the [Reporting API](#operation/fetchReport) — e.g. request the `CampaignId` field on the CAMPAIGN entity.\n\n**Minimal edit example — update budgets for 3 campaigns:**\n```\nEntity Type,Campaign ID,Campaign Daily Budget\nCampaign,1001,150\nCampaign,1002,200\nCampaign,1003,75\n```\n\n---\n\n#### Creating entities\n\nTwo methods are supported. Use **IDs** when you have them (faster); use **hierarchy names** when you don't.\n\n**ID method** — provide the parent entity's Skai ID. This example creates two keywords in an existing ad group (ID 55123):\n```\nEntity Type,Ad Group ID,Keyword,Keyword Match Type,Keyword Bid\nKeyword,55123,running shoes,exact,0.50\nKeyword,55123,buy running shoes,broad,0.40\n```\n\n**Hierarchy method** — provide the name path instead of IDs. This example creates one keyword by identifying its ad group through the Channel Account ID (mandatory), Profile Name, Campaign Name, and Ad Group Name:\n```\nEntity Type,Channel Account ID,Profile Name,Campaign Name,Ad Group Name,Keyword,Keyword Match Type,Keyword Bid\nKeyword,673,Skai Sports - US - Google,Brand Campaign,Brand Ad Group,running shoes,exact,0.50\n```\n\n**Minimum required columns for creation:**\n\n| Entity | ID method | Hierarchy method |\n|---|---|---|\n| Campaign | Channel Account ID + Profile ID + Campaign Name | Channel Account ID + Profile Name + Campaign Name |\n| Ad Group | Campaign ID + Ad Group Name | + Campaign Name + Ad Group Name |\n| Ad | Ad Group ID + Ad Type | + Campaign Name + Ad Group Name + Ad Type |\n| Keyword | Ad Group ID + Keyword + Keyword Match Type | + Campaign Name + Ad Group Name + Keyword + Keyword Match Type |\n\n**Publisher-specific notes:**\n- **Status values differ by publisher type:** `Active` / `Paused` (Apple Search Ads); `Approve` / `Pause` (Retail Media); `Approved` / `Pause` (Search & most Social)\n- **Retail Media creates** require `Campaign Tracking Level = None`\n- Some publishers have additional required fields (e.g. Amazon Sponsored Brands requires `Brand entity ID` or `Brand entity name`; Pinterest and Snapchat require `Campaign Start Date`). Start from an export when working with a new publisher.\n\n---\n\n#### Best practices\n\n**Start from an export.** The fastest way to get the correct column format for a publisher or campaign type is to export existing entities from the Skai UI. The export is identical in format to the import — edit the values you need and re-import. This is especially valuable the first time you work with a new publisher or campaign type.\n\n**Work with the media team first.** When building programmatic bulksheets, get a working example verified in the Skai UI with the media team, then implement the code to generate it at scale.\n\n**Use minimal columns for edits.** You only need the entity ID and the columns you're changing. Omitting unrelated columns reduces errors and makes the file easier to debug.\n\n**Use the error file — it's readable.** When rows fail, retrieve the error file (see below) — you get all failed rows in the original format with a plain-text error description appended as the last column. Errors are specific and actionable. An AI coding agent can read this file, parse the failures, fix the rows, and resubmit without manual intervention.\n\n---\n\n#### Retrieving results\n\nAfter submitting, use the returned `job_id` to poll status and retrieve results.\n\n**1. Poll for completion** — [GET /api/v1/jobs/{job_id}/status](#tag/Jobs/paths/~1api~1v1~1jobs~1{job_id}~1status/get)\nPoll until status is `JOB_COMPLETED` or `JOB_COMPLETED_WITH_FAILURES`.\n\n**2. Get the summary** — [GET /api/v1/jobs/{job_id}/results/file](#tag/Jobs/paths/~1api~1v1~1jobs~1{job_id}~1results~1file/get) with `info_level=SUMMARY`\nReturns a JSON summary:\n```json\n{\n \"elementType\": \"MULTI_ENTITY_FILE\",\n \"processed\": 25,\n \"updated\": 23,\n \"failed\": 2\n}\n```\n\n**3. Get failed rows** — same endpoint with `info_level=BREAKDOWN`\nReturns a zipped CSV with all failed rows in the original import format, plus an error description in the last column." operationId: bulkUpdate parameters: - $ref: '#/components/parameters/ks' requestBody: content: multipart/form-data: schema: type: object properties: file: type: string description: CSV, XLSX, TXT, or TSV — uncompressed or zip-compressed. Maximum size 50 MB. format: binary file_delimiter: type: string description: Column delimiter used in the file. Use `COMMA` for standard CSV. enum: - COMMA - TAB - SEMICOLON bulk_update_type: type: string description: The entity type of the uploaded file. Use `MULTI_ENTITY_FILE` (recommended) to mix entity types in one file using an `Entity Type` column. Use a specific entity type for single-entity files. enum: - MULTI_ENTITY_FILE - CAMPAIGN - AD_GROUP - KEYWORD - AD - PRODUCT_GROUP - PORTFOLIO - SITELINK - AUDIENCES - PROFILE_STATUS - PROFILE_F2A - PROFILE_ONBOARDING required: true responses: 200: description: Job created successfully content: application/json: schema: type: object properties: job_id: type: string description: Use this ID to poll job status and retrieve results. example: job_id: c42f1168-4cd6-42e8-b654-2a7f0043699e 400: $ref: '#/components/responses/BadRequest' 500: $ref: '#/components/responses/InternalServerError' x-code-samples: - lang: cURL source: "curl -X POST \\\n 'https://services.kenshoo.com/api/v1/bulk_update?ks=' \\\n -H 'Authorization: Bearer ' \\\n -F 'file=@changes.csv' \\\n -F 'file_delimiter=COMMA' \\\n -F 'bulk_update_type=MULTI_ENTITY_FILE'\n\n# Response: {\"job_id\": \"c42f1168-4cd6-42e8-b654-2a7f0043699e\"}\n# Then poll: GET /api/v1/jobs/{job_id}/status\n" components: schemas: EntityResponse: type: object properties: id: type: integer format: int64 success: type: boolean errors: type: array items: $ref: '#/components/schemas/ErrorField' ErrorField: type: object properties: fieldName: type: string description: The error field error: type: string description: Error message parameters: type: object additionalProperties: type: string description: Error additional properties ApiResponse: type: object properties: status: $ref: '#/components/schemas/ApiResponseStatus' entities: type: array items: $ref: '#/components/schemas/EntityResponse' ApiResponseStatus: type: string readOnly: true enum: - SUCCESS - FAILED - PARTIAL_SUCCESS responses: BadRequest: description: Bad request (usually indicates validation failure for client input) content: application/json: schema: $ref: '#/components/responses/ApiResponse' example: status: FAILED entities: - id: null success: false errors: - field_name: name error: ILLEGAL_NAME InternalServerError: description: Server error content: application/json: schema: $ref: '#/components/responses/ApiResponse' example: status: FAILED entities: - id: null success: false errors: - field_name: ServerError error: Unexpected error occurred. parameters: {} ApiResponse: $ref: '#/components/schemas/ApiResponse' parameters: ks: name: ks in: query description: The KS to refer the request to. You can find this ID in the Skai platform under _Administration_ -> _About Skai_ -> _Server ID_ required: true style: form explode: true schema: type: string example: '1234' x-tagGroups: - name: Reporting tags: - Available Columns - Synchronous Reports - Asynchronous Reports - name: Bulk Operations tags: - Jobs - Bulk Update - name: AI & MCP tags: - MCP - name: Objects tags: - Profile - Campaigns - Ad Groups - Ads - Product Groups - Portfolios - Meta Campaigns - Meta Ad Groups - Meta Ads - Columns