Rezolve Ai
Guides

Feeds API

Create a feed over the API, keep it current from a URL or with signed pushes, publish it to channels, and read back what Rezolve Ai wrote.

The /api/v1/feeds endpoints let a catalogue that is not on Shopify or WooCommerce live in Rezolve Ai without anyone uploading a file. You create a feed, keep its products current, and pull the enriched result back out, either as a file a channel accepts or field by field for your own PIM.

This guide walks through the whole loop. Each step links to its reference page, which has the full schema and a live request panel.

Before you start

You need an API key with the feeds scopes:

ScopeEndpoints
feeds:writeCreate a feed, set or refresh its source, rotate the ingest secret, change delivery, delete a feed
feeds:readList feeds, read a feed, read source and delivery settings, download an export, read enrichments

feeds:write also satisfies a feeds:read check, and admin satisfies both. These scopes are separate from the enrichment scopes on purpose: a key that only moves catalogue data cannot spend credits or control jobs.

Tick feeds:read and feeds:write under Scopes when you create the key. They are not part of the default selection. See API Keys.

A feed belongs to one project. Pin the key to that project and you never have to name it. With an account-wide key, send projectId when you create the feed. See Project scope.

export ACE_API_KEY=ace_live_your_key_here

None of the feeds endpoints is charged. See Credits and usage for the one indirect cost, an Autopilot run started by a push.

How the pieces fit

  1. Create a feed. You get a feedId and, by default, a signing secret for pushes.
  2. Fill it. Point it at a file you host and let Rezolve Ai pull it on a schedule, push changed products as they change, or do both.
  3. Enrich it. Content is generated by Autopilot or from the app, and approved in Review.
  4. Take the result out. Publish channel URLs for Google, Microsoft, or an agentic commerce console to fetch, download an export yourself, or read the generated content per field.
  5. Subscribe to the feed webhooks so you pull only when something changed.

The source and delivery endpoints set the same things as the Sources and Delivery cards on the feed in the app.

The Sources card for a feed, including Push changes to us, beside the Delivery card

Create a feed

POST /api/v1/feeds creates the feed, sets its source, and issues the signing secret in one call. It needs feeds:write.

curl -X POST https://ace.authoritas.com/api/v1/feeds \
  -H "Authorization: Bearer $ACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main catalogue",
    "source": "url",
    "sourceUrl": "https://example.com/feeds/products.xml",
    "refreshCron": "0 */6 * * *",
    "refreshTimezone": "Europe/London",
    "refreshNow": true
  }'
FieldTypeNotes
namestringRequired, up to 200 characters. The feed's display name, and its identity within the project.
projectIdstringRequired for an account-wide key. A pinned key uses its own project.
sourceapi or urlDefaults to api, a feed that waits for pushes. url is a feed pulled from a file you host. A feed can do both.
sourceUrlstringRequired when source is url. Must be an http or https address that is reachable from the public internet. Private and internal addresses are rejected.
refreshCronstringA standard cron expression. The schedule must leave at least 30 minutes between runs.
refreshTimezonestringThe timezone the cron is read in. Defaults to UTC.
createIngestSecretbooleanDefaults to true. Issues the secret that signs pushes.
refreshNowbooleanDefaults to false. Pulls sourceUrl straight away rather than waiting for the schedule.

The response is 201 for a new feed:

{
  "data": {
    "feedId": "<feed-id>",
    "projectId": "<project-id>",
    "name": "Main catalogue",
    "source": "url",
    "eventsUrl": "/api/v1/feeds/<feed-id>/events",
    "ingestSecret": "<shown once>",
    "sourceError": null,
    "created": true
  }
}

Keep feedId: every later call addresses the feed by it.

ingestSecret is returned once, in this response only. Store it before you do anything else. If you lose it, the only way to get another is to rotate it.

Two things to know:

  • The call is safe to repeat. Creating a feed with a name the project already has returns that feed with status 200 and "created": false. Nothing about the feed changes: its source, its schedule, its column mapping and its signing secret all stay as they were, and ingestSecret is null. To change the source, use the next endpoint. To get a new secret, rotate it.
  • sourceError is not a failure of the whole call. If the URL or schedule was rejected, the feed is still created and sourceError says why. Fix the source with the next endpoint.

Set or replace the source

PUT /api/v1/feeds/{id}/source changes where the feed is pulled from and how often. It needs feeds:write.

curl -X PUT https://ace.authoritas.com/api/v1/feeds/$FEED_ID/source \
  -H "Authorization: Bearer $ACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sourceUrl": "https://example.com/feeds/products-v2.xml", "refreshCron": "0 3 * * *" }'
  • A field you leave out is left alone.
  • A field you send as null is cleared.
  • Clearing sourceUrl clears the schedule with it, because a schedule with nothing to fetch could never run.
  • The URL is checked when you save it, so an unreachable or internal address is refused with 400 here rather than failing later.

The response is the feed's source settings, the same shape GET /api/v1/feeds/{id}/source returns (feeds:read):

{
  "data": {
    "feedId": "<feed-id>",
    "sourceType": "url",
    "sourceUrl": "https://example.com/feeds/products-v2.xml",
    "refreshCron": "0 3 * * *",
    "refreshTimezone": "Europe/London",
    "nextRefreshAt": "2026-10-10T02:00:00.000Z",
    "lastRefreshAt": "2026-10-09T02:00:04.000Z",
    "lastRefreshStatus": "unchanged",
    "lastRefreshError": null,
    "ingest": {
      "configured": true,
      "active": true,
      "url": "https://ace.authoritas.com/api/v1/feeds/<feed-id>/events",
      "lastSeenAt": null,
      "lastError": null,
      "eventsReceived": 0,
      "rowsUpserted": 0,
      "rowsDeleted": 0
    }
  }
}

lastRefreshStatus is ingesting when a changed file was fetched and handed to ingestion, unchanged when the file was the same as last time, and failed when the pull did not work, with the reason in lastRefreshError. The ingest block reports on pushes: whether a signing secret exists, when the last push arrived, and running totals.

Scheduled pulls are picked up every 15 minutes, so a refresh starts within about 15 minutes of its scheduled time. A pull of an unchanged file does no further work: nothing is ingested again and nothing is enriched again.

Refresh now, or rotate the secret

POST /api/v1/feeds/{id}/source takes an action. It needs feeds:write.

# Pull the source URL now, even if the file has not changed.
curl -X POST https://ace.authoritas.com/api/v1/feeds/$FEED_ID/source \
  -H "Authorization: Bearer $ACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "refresh" }'
# => { "data": { "dispatched": true } }

# Issue a new signing secret. It is returned once, as data.secret.
curl -X POST https://ace.authoritas.com/api/v1/feeds/$FEED_ID/source \
  -H "Authorization: Bearer $ACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "rotate-secret" }'

refresh returns 400 if the feed has no source URL. After rotate-secret, anything still signing with the old secret gets 401.

Push changed products

POST /api/v1/feeds/{id}/events takes only the products that changed. Send it from wherever your platform already knows about changes, for example a webhook worker.

This endpoint is not authenticated with an API key. Each request is signed with the feed's ingest secret:

  1. Build the JSON body.
  2. Compute an HMAC-SHA256 of the exact bytes you are about to send, keyed with the ingest secret.
  3. Send the lowercase hex digest in the X-Ace-Signature header.
BODY='{"upserts":[{"id":"sku-1","title":"Merino base layer","price":79,"currency":"GBP"}],"deletes":["sku-9"]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$FEED_INGEST_SECRET" | awk '{print $NF}')

curl -X POST "https://ace.authoritas.com/api/v1/feeds/$FEED_ID/events" \
  -H "Content-Type: application/json" \
  -H "X-Ace-Signature: $SIG" \
  -d "$BODY"

The same in Node:

import crypto from "node:crypto";

const body = JSON.stringify({
  upserts: [{ id: "sku-1", title: "Merino base layer", price: 79, currency: "GBP" }],
  deletes: ["sku-9"],
});
const signature = crypto
  .createHmac("sha256", process.env.FEED_INGEST_SECRET)
  .update(body, "utf8")
  .digest("hex");

await fetch(`https://ace.authoritas.com/api/v1/feeds/${feedId}/events`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Ace-Signature": signature },
  body, // send the string you signed, not a re-serialised copy
});

The body has two optional arrays, and needs at least one entry between them:

FieldTypeNotes
upsertsproduct[]Products to create or update.
deletesstring[]Your own product ids to remove from this feed.

A product needs id and title. It can also carry description, category, productType, brand, vendor, price (a number), currency, availability, tags, images (each with src and optional alt), variants (each with optional title, price, sku, inventoryQuantity), and attributes. Keys inside attributes are treated as top-level fields, and any other key you send is kept as extra grounding for generated content.

The response is 200:

{
  "data": {
    "feedId": "<feed-id>",
    "upserted": 1,
    "created": 0,
    "updated": 1,
    "deleted": 1,
    "enrichment": "skipped"
  }
}

Rules worth knowing:

  • A push never removes what it does not mention. Products you leave out are untouched. A removal has to be named in deletes.
  • At most 1,000 rows per request, counting upserts and deletes together. A larger request is refused with 400, not truncated. Split the batch, or use a scheduled URL refresh for a whole catalogue.
  • A product id identifies one product in the project. Pushing an id the project already holds updates that product.
  • Deletes are safe to repeat. An id that is not in the feed is ignored, and deleted counts what was actually removed.
  • enrichment is dispatched when the push started an Autopilot run for the upserted products, and skipped otherwise. A run starts only when Autopilot is on for the feed, Re-enrich when source data changes is ticked, and Autopilot has content types to write. Pushes that arrive in the same minute share one run.
  • A failed signature is always 401, with the same answer whether the secret is wrong, the feed has no secret, or the feed does not exist.

Publish to channel URLs

PUT /api/v1/feeds/{id}/delivery turns on hosted feed URLs: stable addresses you register once with a channel, which then fetches the feed on its own schedule. It needs feeds:write.

curl -X PUT https://ace.authoritas.com/api/v1/feeds/$FEED_ID/delivery \
  -H "Authorization: Bearer $ACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true, "channels": ["google-shopping", "microsoft-shopping"] }'
FieldTypeNotes
enabledbooleanTurns delivery on or off.
channelsstring[]Replaces the list of published channels. Valid values: google-shopping, agentic-commerce, microsoft-shopping, shopify, woocommerce.
rotateTokenbooleanIssues new URLs. Every URL already registered elsewhere stops working.
{
  "data": {
    "enabled": true,
    "channels": ["google-shopping", "microsoft-shopping"],
    "urls": {
      "google-shopping": "https://ace.authoritas.com/api/feeds/f/<token>/google-shopping",
      "microsoft-shopping": "https://ace.authoritas.com/api/feeds/f/<token>/microsoft-shopping"
    },
    "availableChannels": ["google-shopping", "agentic-commerce", "microsoft-shopping", "shopify", "woocommerce"]
  }
}

GET /api/v1/feeds/{id}/delivery returns the same shape (feeds:read). urls is null while delivery is off.

About the hosted URLs:

  • Possession of the URL is the access control. The URLs need no key, so treat them like a secret you share only with the channel. Use rotateToken if one leaks.
  • Only approved content is served. The file is your catalogue with the content you have approved merged in.
  • Google Shopping and Microsoft Shopping URLs serve Google Merchant RSS 2.0 XML by default. Add ?format= to ask for another format the channel supports.
  • Responses are cached for five minutes, so a change can take that long to show.
  • A URL answers 404 when delivery is off or the channel is not in channels.

Download an export

GET /api/v1/feeds/{id}/export returns the feed as the file a channel accepts, with your approved content merged in. It needs feeds:read. The response is the file itself, not a JSON envelope, so it can be piped straight to disk.

curl -s "https://ace.authoritas.com/api/v1/feeds/$FEED_ID/export?channel=google-shopping&format=csv" \
  -H "Authorization: Bearer $ACE_API_KEY" \
  -D headers.txt \
  -o google-shopping.csv
ParameterNotes
channelRequired. google-shopping, agentic-commerce, microsoft-shopping, shopify, or woocommerce.
formatjson, xml, csv, or tsv. Defaults to the channel's own format.
rss1 for Google Merchant RSS 2.0, the format Merchant Center subscribes to.
ChannelDefault formatFormats accepted
google-shoppingjsonjson, xml, csv, tsv
agentic-commercetsv, as the flat file the channel ingestsjson, xml, csv, tsv
microsoft-shoppingcsvcsv, tsv
shopifycsvcsv, tsv, json
woocommercecsvcsv, tsv, json

A format the channel does not support returns 400 with the list it does. Errors use the usual JSON error envelope.

Counts come back as headers so the body stays a clean file:

HeaderMeaning
X-Ace-Product-CountRows in the file.
X-Ace-Issue-CountRow-level problems the channel is likely to reject, such as a missing required field.
X-Ace-Truncated1 when the feed holds more than 50,000 products and the file was capped, otherwise 0.

Read enrichments

GET /api/v1/feeds/{id}/enrichments returns what Rezolve Ai wrote, per product and per field, with the value before and after. Use it when you want to map content into your own PIM instead of taking a channel file. It needs feeds:read.

curl "https://ace.authoritas.com/api/v1/feeds/$FEED_ID/enrichments?since=2026-10-01T00:00:00Z&limit=100" \
  -H "Authorization: Bearer $ACE_API_KEY"
ParameterNotes
statusapproved (the default), pending, or rejected. Selects products that have content in that state.
sinceAn ISO 8601 timestamp. Only content changed after it is considered. Use your own watermark.
limit1 to 200, default 50.
cursorThe nextCursor from the previous page.
{
  "data": {
    "feedId": "<feed-id>",
    "products": [
      {
        "productId": "sku-1",
        "updatedAt": "2026-10-09T09:12:44.000Z",
        "approvedItems": 2,
        "totalItems": 3,
        "fields": [
          {
            "field": "description",
            "before": "Noise cancelling, 20h battery.",
            "after": "Wireless headphones with active noise cancelling and a 20 hour battery.",
            "approved": true,
            "confidence": 0.9,
            "source": null
          }
        ]
      }
    ]
  },
  "meta": { "cursor": { "limit": 100, "hasMore": true, "nextCursor": "<opaque>" } }
}

How to page it:

  • Follow the cursor, not a page number. Pass nextCursor back as cursor until hasMore is false. Content is written while you page, and offset paging would repeat and skip rows. Send the cursor back unchanged: one that cannot be read is refused with 400.
  • limit counts content items, not products. A product has one item per content type, so a page can hold fewer products than limit, and the same product can appear on more than one page. Write by productId.
  • Check approved on each field. fields lists every field Rezolve Ai has written for the product, whichever status you asked for. Only a field with "approved": true is content you have signed off. confidence runs from 0 to 1.
  • Keep your own watermark. Store the newest updatedAt you have processed and send it as since next time. updatedAt is when the content on that page last changed, which is the same clock since reads.

Feed webhooks

Three webhook events tell you when a feed has something new, so you do not have to poll. Subscribe to them like any other event.

EventSent when
feed.source.refreshedA pull of the source URL finished, scheduled or requested.
feed.autopilot.completedAn Autopilot run for the feed finished: its content is written and the quality check has run.
feed.autopilot.failedAn Autopilot run found products to work on but could not start.

feed.source.refreshed carries data.feedId and data.kind:

kindMeaningAlso in data
ingestingThe file changed and was handed to ingestion.jobId
unchangedThe file was the same as last time.via: not-modified or same-content
skippedThe feed has no source URL.reason

An unchanged pull is reported on purpose, so a quiet source can be told apart from a broken one. A pull that fails sends no event: look at lastRefreshStatus and lastRefreshError on the source settings.

The Autopilot events carry:

{
  "event": "feed.autopilot.completed",
  "timestamp": "2026-10-09T10:15:00.000Z",
  "project_id": "<project-id>",
  "data": {
    "feedId": "<feed-id>",
    "runId": "<run-id>",
    "trigger": "scheduled",
    "cause": null,
    "productsEvaluated": 420,
    "productsQueued": 37,
    "skippedApproved": 12,
    "truncated": false,
    "jobIds": ["<job-id>"],
    "error": null,
    "resultsUrl": "/api/v1/feeds/<feed-id>/enrichments?status=approved",
    "approved": 31,
    "held": 6
  }
}
  • trigger is scheduled or manual. A run started by a push is manual with cause set to delta.
  • productsQueued is how many products the run sent for generation. On feed.autopilot.failed it is 0 and error says why.
  • feed.autopilot.completed is sent when the run has finished: the content is written and Autopilot's quality check has run. approved is how many items the check approved, which is what resultsUrl serves straight away. held is how many it left for a person. Both are 0 when automatic approval is off for the feed: every item then waits in Review, and reaches resultsUrl as it is approved, so keep reading the enrichments endpoint with your since watermark.
  • feed.autopilot.failed carries the same fields without approved and held.
  • A run whose generation job stops part way without finishing sends neither event. The run's outcome is on the feed's Autopilot history in the app.
  • A run that finds nothing to do, or that is waiting for another run on the project to finish, sends no event.

Other feed endpoints

  • GET /api/v1/feeds lists a project's feeds (feeds:read).
  • GET /api/v1/feeds/{id} returns one feed with its products and warnings (feeds:read).
  • DELETE /api/v1/feeds/{id} deletes a feed (feeds:write). The response returns immediately and the products are removed in the background. A key pinned to a project can delete only that project's feeds.

The first two need projectId unless your key is pinned to a project. See Project scope.

See also

On this page