Skip to navigation

API export

Receive produced and enriched content from Etals on an endpoint you control.
View as Markdown

When a task in Etals finishes, Etals can send the produced content straight to your system. Etals makes an HTTPS POST with a JSON body to a URL you give us. This is the Custom API integration, often called “the webhook”.

Your side is one receiver endpoint: check the auth header, store the values, answer 200.

Who sets it up

The Custom API integration is configured by Etals. Send your Etals contact:

SettingWhat to send us
API URLThe HTTPS URL Etals should POST to.
Mirror URLsOptional. Extra URLs that receive a copy of every request, for example a staging receiver. See Mirror URLs.
API keyThe secret Etals sends in the Authorization header. See Securing your endpoint.
Batch sizeOptional. A lower maximum number of items per request if your receiver needs it.
All localizationsOptional. Whether each exported property should also carry the current values of its other localizations. See All localizations.
Content typesWhich content types (for example Products) should export to your endpoint. Etals connects each of them to the Custom API integration.

Only properties that are allowed to leave Etals are sent:

  • Properties created in Etals start as Internal only. Switch each property you want to receive to Exportable on its property page. Properties that stay Internal only are never sent.
  • Properties that an import created, for example a file import, belong to that import and are exported without the switch. Check the switch for every property you expect to receive.

Etals can also replace the payload with a custom shape for your workspace. The rest of this page describes the standard payload.

When Etals sends data

An export starts when a task that is set to publish has its results published:

  • Automations. In the automation builder, under After it is written, Publish to your connected platform sends the results when the task completes. Save as drafts keeps the results in Etals and sends nothing. The two choices only show once the content type is connected to an integration, such as your Custom API export. Without one, results are always kept as drafts. When Publish to your connected platform is selected, the summary next to the builder reads Exports to Custom API when a task completes.
  • Approval. When Needs approval first is on, the task completes, and exports, after a person has approved the content.
  • Tasks run in the grid. New values are committed when a run is done. The Export to Custom API switch in the Generate window, or Also export to Custom API under Finish task, decides whether they are also sent to your endpoint.
  • Feedback tasks. Tasks created from the Feedback API export when they finish. See that page for the exact rules.
  • Retries. See Export status and retries.

The After it is written section of the automation builder, with Publish to your connected platform selected and Needs approval first switched off

The request

POST {your-api-url}
Content-Type: application/json
Authorization: Bearer {your-api-key}
  • The Authorization header is only sent when an API key is configured. A key starting with Bearer or Basic is sent as is. Any other key is sent as Bearer {key}.
  • No other custom headers are sent, and requests are not signed. There is no Accept header.
  • The User-Agent is the generic one of Etals’ HTTP client (GuzzleHttp/7 today). Do not rely on it to identify Etals.

The payload

This is a real request from the Nordvik Outdoor demo workspace, trimmed to one product. An automation wrote the English and Swedish description and the meta title and meta description for the UK and Swedish websites of NV-1001.

{
"attributables": [
{
"attributable_type": {
"id": 34,
"system_code": "products"
},
"items": [
{
"id": 1,
"identifiers": {
"identifier": "NV-1001"
},
"properties": [
{
"property_id": 80,
"system_code": "description",
"values": [
{
"language": "en",
"value_id": 457,
"value": "The Fjord Shell Jacket is made for women heading out on wet trails and long days in bad weather. This 3-layer shell is the toughest kind of rain shell, built to protect you from wind and heavy rain.\n\nThe fabric keeps out heavy rain and still lets sweat out, with a 20k/20k rating and fully taped seams. Underarm pit zips help you regulate your temperature on uphill climbs, while the helmet-compatible hood stays secure in strong wind. The jacket is made from recycled polyester with a PFC-free water-repellent finish.\n\nWash at 30 degrees and tumble dry low to refresh the water repellency."
}
]
},
{
"property_id": 81,
"system_code": "meta_title",
"values": [
{
"brand": "nordvik-outdoor",
"market": "uk",
"front": "nordvik-outdoor-uk",
"language": "en",
"value_id": 459,
"value": "Fjord Shell Jacket: Waterproof Shell | Nordvik Outdoor"
},
{
"brand": "nordvik-outdoor",
"market": "se",
"front": "nordvik-outdoor-se",
"language": "sv",
"value_id": 460,
"value": "Fjord Shell Jacket | Håller dig torr | Nordvik Outdoor"
}
]
},
{
"property_id": 82,
"system_code": "meta_description",
"values": [
{
"brand": "nordvik-outdoor",
"market": "uk",
"front": "nordvik-outdoor-uk",
"language": "en",
"value_id": 461,
"value": "Stay dry on wet trails with this 3-layer waterproof shell. Fully taped seams and recycled fabric keep heavy rain out so you can keep moving."
}
]
}
]
}
]
}
]
}

The full request also carried the Swedish description and the Swedish meta description. A request with more products has more entries in items.

Fields

FieldTypeAlways presentMeaning
attributablesarrayYesOne entry per content type. A request carries one content type.
attributable_type.idintegerYesEtals ID of the content type, for example 34.
attributable_type.system_codestringYesSystem code of the content type, for example products.
itemsarrayYesThe items in this request. Items without any exported value are left out.
items[].idintegerYesEtals’ internal ID of the item.
items[].identifiers.identifierstringYesYour identifier for the item, for example NV-1001: the product number for products, the identifier for other content types. This is the identifier you sent through the Funnel API.
items[].propertiesarrayYesThe exported properties of the item.
properties[].property_idintegerYesEtals ID of the property.
properties[].system_codestringYesSystem code of the property, for example meta_title.
properties[].valuesarrayYesOne entry per localization that was produced.
values[].languagestringNoLanguage system code, for example sv. Left out when the property is not language specific.
values[].marketstringNoMarket system code, for example se. Left out when not market specific.
values[].brandstringNoBrand system code, for example nordvik-outdoor. Left out when not brand specific.
values[].frontstringNoWebsite (front) system code, for example nordvik-outdoor-se. Left out when not website specific.
values[].value_idintegerNoID of this produced value. Every value a task produced has one. Only values added by All localizations have none. Store it if you want to send feedback.
values[].valuestring or arrayYesThe content. See below.

Which localization keys a value carries follows how the property is set up:

  • A property written per language, like the description above, carries only language.
  • A property written per website, like the meta title and meta description above, carries brand, market, front and language together.

The codes are the system codes set up in your workspace, not ISO codes. In the Nordvik Outdoor workspace, for example, Norwegian is no (not nb) and the UK market is uk. Ask your Etals contact for the list, or read them with the read endpoints GET /api/v1/languages, /markets, /brands and /fronts. Do not rely on the order of keys inside a value.

Values

  • Text properties send a string. Line breaks are sent as \n, as in the description above.
  • HTML properties send the value exactly as Etals stores it, as a string. Etals does not add, strip or convert markup on the way out. If your prompts write HTML you receive HTML, if they write plain paragraphs you receive plain paragraphs. The description in the example is an HTML property whose content is plain text.
  • Option properties send an array with the stored value of each selected option, not Etals’ option IDs. For options that came from your own data this is the value you sent. If an option was removed after the task ran, its slot in the array can be null.
  • A localization the task produced no result for is left out, rather than sent as null.

The body is standard JSON, encoded the way PHP encodes it: characters outside ASCII are escaped and / is escaped. The Swedish meta title above arrives as "Fjord Shell Jacket | H\u00e5ller dig torr | Nordvik Outdoor" and 20k/20k as 20k\/20k. Any JSON parser turns these back into the original text, so parse the body instead of matching on the raw bytes.

All localizations

When the All localizations setting is on, every exported property also carries the current stored values of its other localizations. Those extra values have no value_id, because they were not produced by this task. For example, a task that only wrote the UK meta description of NV-1001 sends the UK value with its value_id, followed by the stored Swedish, Norwegian, Danish and German meta descriptions without one.

Batches

  • One task can lead to several requests.
  • A request holds at most 100 items. Etals can set a lower limit with the batch size setting. With a batch size of 1, a task with two products sends two requests.
  • Large tasks are split into chunks (1,000 items by default) that are exported in parallel. Do not assume requests arrive in any particular order.
  • Each request is independent. There is no “start” or “end” message.

Mirror URLs

When mirror URLs are set, Etals sends the same request, with the same body and headers, to each mirror first and then to the main URL. This happens for every batch. Mirror responses are logged but ignored: a mirror that answers 500 does not fail the export. Only the main URL decides the export status.

How Etals reads your response

Your responseExport status of the items in that requestError stored in Etals
Any 2xxCompleted
422FailedWrong payload format: {your response body}
Any other statusFailedAPI request failed with status {status} : {your response body}
No connection within 10 seconds, or no full response within 30 seconds (redirects included)FailedAPI request failed with exception: {error}
A response body over 16 MBFailedAPI request failed with exception: The response exceeds the configured size limit.

Etals only sends to public addresses. A URL that isn’t http or https, can’t be resolved, or points to a private or internal network fails without being called, for example with API request failed with exception: The address points to a private or internal network and was not fetched: {url}. The same check applies to mirror URLs and to redirects.

The response body is only used as the error text, so return a short, readable message on errors. For example, a 500 with the body Receiver database is down, try again later is stored as API request failed with status 500 : Receiver database is down, try again later. When your endpoint cannot be reached at all, the stored error is the HTTP client’s message and includes your URL, for example API request failed with exception: cURL error 7: Failed to connect to ....

Etals does not retry a failed export on its own. Failed items stay failed until the export is sent again, see Export status and retries. Answer 2xx as soon as you have stored the request, and do the heavy work afterwards.

Items in a task that are not part of the export get the status Skipped with the message This property is not exportable. It can be made exportable in property settings.

Export status and retries

Where you see the export status depends on the kind of task.

Automation tasks open as a grid. Each committed value shows its export status above its text: Committed and exported to Custom API, Exporting to Custom API, Export waits for review or Export failed. Hovering Export failed shows the stored error, for example API Endpoint: API request failed with status 500 : Receiver database is down, try again later. When values failed, a notice at the bottom of the grid also reads, for example, Custom API: 1 value not exported, with the first error.

An automation task in the grid with the English and Swedish meta titles of NV-1001 and NV-1002, each marked Committed and exported to Custom API

  • A failed export does not change the task status. The task still shows Finished, so look at the values.
  • The grid has no retry button. Ask your Etals contact to resend the failed items, or fix the cause and generate the values again with Export to Custom API switched on.

Feedback tasks and tasks created through the API also open as a grid once they finish. The grid doesn’t show their export status, and there are no retry buttons for single items or for failed items only. To send such a task again, click Export at the top of the grid, choose Integration and follow the steps. Etals then sends every value of the task again, with its current content.

Duplicates and ordering

Plan for receiving the same content more than once. It happens on every retry, when a feedback task regenerates a value, and when a request timed out on our side after your receiver already stored it.

  • Key what you store on identifier + property system_code + localization (language, market, brand, front). Treat each value as an upsert of that key.
  • value_id identifies one produced value in one task. A retry sends the same value_id again. If someone edited or regenerated that value inside the same task before it was sent again, the same value_id arrives with new content, so store the content you receive rather than skipping known value_ids.
  • A later task gets new, higher value_ids. If two requests carry values with different value_ids for the same key, the one with the higher value_id is newer.
  • Values sent because of All localizations have no value_id. Treat them as the current value at the time of sending.

Securing your endpoint

Requests are not signed. Protect your endpoint like this:

  • Use HTTPS only.
  • Give Etals a long random secret as the API key and reject requests whose Authorization header does not match. Both Bearer tokens and Basic credentials work.
  • Compare the header in constant time, and do not log it.
  • If you need an IP allowlist, ask your Etals contact which addresses exports come from before you rely on one.
  • Do not redirect the export URL. Point Etals at the final URL.

Minimal receiver

Both examples check the Authorization header, walk attributables[].items[].properties[].values[], store each value keyed as described above, and answer quickly.

import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json({ limit: "20mb" }));
const expected = Buffer.from(`Bearer ${process.env.ETALS_EXPORT_SECRET}`);
function authorized(header = "") {
const given = Buffer.from(header);
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
app.post("/etals/export", async (req, res) => {
if (!authorized(req.get("authorization"))) {
return res.status(401).send("Unauthorized");
}
const rows = [];
for (const group of req.body.attributables ?? []) {
for (const item of group.items) {
for (const property of item.properties) {
for (const v of property.values) {
rows.push({
contentType: group.attributable_type.system_code,
identifier: item.identifiers.identifier,
property: property.system_code,
language: v.language ?? null,
market: v.market ?? null,
brand: v.brand ?? null,
front: v.front ?? null,
valueId: v.value_id ?? null,
value: v.value,
});
}
}
}
}
try {
await saveForLaterProcessing(rows);
res.status(200).send("OK");
} catch (error) {
res.status(500).send(`Could not store export: ${error.message}`);
}
});
app.listen(3000);

saveForLaterProcessing stands for your own code: write the rows to a queue or table, and update your product data from there.

Feedback on exported values

If a reviewer or a check in your system finds a problem with a value, send it back with the value_id, or with the identifier, property and localization. Etals stores the feedback and can regenerate the value. See API Feedback.