curl --request POST \
--url https://api-dev.narrative.io/webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "webhook_subscription_jobs",
"url": "<string>",
"job_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"job_types": [
"datasets_deliver_data"
],
"states": [],
"name": "<string>"
}
'import requests
url = "https://api-dev.narrative.io/webhooks"
payload = {
"type": "webhook_subscription_jobs",
"url": "<string>",
"job_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"job_types": ["datasets_deliver_data"],
"states": [],
"name": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'webhook_subscription_jobs',
url: '<string>',
job_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
job_types: ['datasets_deliver_data'],
states: [],
name: '<string>'
})
};
fetch('https://api-dev.narrative.io/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-dev.narrative.io/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'type' => 'webhook_subscription_jobs',
'url' => '<string>',
'job_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'job_types' => [
'datasets_deliver_data'
],
'states' => [
],
'name' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-dev.narrative.io/webhooks"
payload := strings.NewReader("{\n \"type\": \"webhook_subscription_jobs\",\n \"url\": \"<string>\",\n \"job_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"job_types\": [\n \"datasets_deliver_data\"\n ],\n \"states\": [],\n \"name\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-dev.narrative.io/webhooks")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"webhook_subscription_jobs\",\n \"url\": \"<string>\",\n \"job_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"job_types\": [\n \"datasets_deliver_data\"\n ],\n \"states\": [],\n \"name\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-dev.narrative.io/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"webhook_subscription_jobs\",\n \"url\": \"<string>\",\n \"job_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"job_types\": [\n \"datasets_deliver_data\"\n ],\n \"states\": [],\n \"name\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"summary": "Job subscription with all three filters set",
"value": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"app_id": null,
"name": "job_updates",
"secret": "8b6dba8c-9a8b-4a3a-8469-9dc7a33c3e17",
"status": "active",
"company_id": 42,
"job_ids": {
"values": [
"9a3cf334-4c6d-4d3f-a7d6-22b6e6f2ac1b",
"d35a12c1-7a2f-46b9-8d60-9e0a2a84c205"
]
},
"job_states": {
"values": [
"completed",
"failed"
]
},
"job_types": {
"values": [
"datasets_deliver_data",
"materialize-view"
]
},
"url": "https://webhooks.mycompany.com/events",
"created_at": "2025-04-29T16:12:45.123456",
"updated_at": "2025-04-29T16:12:45.123456"
}
}{
"error": "Unauthorized",
"error_description": "You are not authorized to use this endpoint."
}{
"error": "Unauthorized",
"error_description": "You are not authorized to use this endpoint."
}Create a webhook subscription
Create a webhook subscription for your company. The response includes a secret; store it, because it is what
lets you verify that a later callback really came from Narrative.
Requires write access to webhooks.
curl --request POST \
--url https://api-dev.narrative.io/webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "webhook_subscription_jobs",
"url": "<string>",
"job_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"job_types": [
"datasets_deliver_data"
],
"states": [],
"name": "<string>"
}
'import requests
url = "https://api-dev.narrative.io/webhooks"
payload = {
"type": "webhook_subscription_jobs",
"url": "<string>",
"job_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"job_types": ["datasets_deliver_data"],
"states": [],
"name": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'webhook_subscription_jobs',
url: '<string>',
job_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
job_types: ['datasets_deliver_data'],
states: [],
name: '<string>'
})
};
fetch('https://api-dev.narrative.io/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-dev.narrative.io/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'type' => 'webhook_subscription_jobs',
'url' => '<string>',
'job_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'job_types' => [
'datasets_deliver_data'
],
'states' => [
],
'name' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-dev.narrative.io/webhooks"
payload := strings.NewReader("{\n \"type\": \"webhook_subscription_jobs\",\n \"url\": \"<string>\",\n \"job_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"job_types\": [\n \"datasets_deliver_data\"\n ],\n \"states\": [],\n \"name\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-dev.narrative.io/webhooks")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"webhook_subscription_jobs\",\n \"url\": \"<string>\",\n \"job_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"job_types\": [\n \"datasets_deliver_data\"\n ],\n \"states\": [],\n \"name\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-dev.narrative.io/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"webhook_subscription_jobs\",\n \"url\": \"<string>\",\n \"job_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"job_types\": [\n \"datasets_deliver_data\"\n ],\n \"states\": [],\n \"name\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"summary": "Job subscription with all three filters set",
"value": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"app_id": null,
"name": "job_updates",
"secret": "8b6dba8c-9a8b-4a3a-8469-9dc7a33c3e17",
"status": "active",
"company_id": 42,
"job_ids": {
"values": [
"9a3cf334-4c6d-4d3f-a7d6-22b6e6f2ac1b",
"d35a12c1-7a2f-46b9-8d60-9e0a2a84c205"
]
},
"job_states": {
"values": [
"completed",
"failed"
]
},
"job_types": {
"values": [
"datasets_deliver_data",
"materialize-view"
]
},
"url": "https://webhooks.mycompany.com/events",
"created_at": "2025-04-29T16:12:45.123456",
"updated_at": "2025-04-29T16:12:45.123456"
}
}{
"error": "Unauthorized",
"error_description": "You are not authorized to use this endpoint."
}{
"error": "Unauthorized",
"error_description": "You are not authorized to use this endpoint."
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
- Option 1
- Option 2
A subscription is either job-based or app-based, chosen by type. The two kinds carry different filters and
cannot be mixed in one subscription.
webhook_subscription_jobs The HTTPS endpoint Narrative POSTs each event to. It should return a 2xx quickly; anything else is retried.
Deliver events only for these jobs. Omit to match any job.
1Unique identifier for the job.
Deliver events only for jobs of these types. Omit to match any type. Job types are open-ended strings —
new ones are added as the platform grows — so this is not validated against a fixed list. Common values
include datasets_deliver_data, datasets_sample, datasets_calculate_column_stats,
datasets_delete_table, materialize-view, model_training_run, model_inference_run and
nql-forecast.
1255Deliver events only when a job enters one of these states. Omit to match any state.
1Where a job is in its lifecycle.
pending— enqueued and waiting to be picked up.scheduled— claimed by an executor but not yet started.running— being executed.pending_cancellation— cancellation has been requested and the data plane has yet to stop the work.completed,cancelled,failed— terminal states.
pending, scheduled, running, pending_cancellation, completed, cancelled, failed A label for your own use. Narrative does not interpret it.
Response
The subscription was created.
A webhook subscription. The job_* filters are only set on job subscriptions and app_id only on app
subscriptions; the unused ones come back as null rather than being omitted.
Sent back on every delivery in the X-Narrative-Secret header. Compare it against this value to confirm a
callback came from Narrative.
Archiving is one-way. The read endpoints only return active subscriptions, so in practice this is always
active — an archived subscription responds 404.
active, archived The company that owns the subscription.
The endpoint events are delivered to.
An ISO 8601 local date-time with no offset or zone — 2025-04-29T16:12:45.123456. Read it as UTC. This is
deliberately not format: date-time, which means RFC 3339 and would require an offset the server does not send.
Note that the delivered event envelope uses a different format: its created_at does carry a Z.
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$"2025-04-29T16:12:45.123456"
An ISO 8601 local date-time with no offset or zone — 2025-04-29T16:12:45.123456. Read it as UTC. This is
deliberately not format: date-time, which means RFC 3339 and would require an offset the server does not send.
Note that the delivered event envelope uses a different format: its created_at does carry a Z.
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$"2025-04-29T16:12:45.123456"
The app this subscription follows. Null on job subscriptions.
The label supplied at creation time. Null if none was given.
The job_ids filter, or null if the subscription does not filter by job.
Show child attributes
Show child attributes
{
"values": [
"9a3cf334-4c6d-4d3f-a7d6-22b6e6f2ac1b",
"d35a12c1-7a2f-46b9-8d60-9e0a2a84c205"
]
}
The states filter, or null if the subscription does not filter by state.
Show child attributes
Show child attributes
{ "values": ["completed", "failed"] }
The job_types filter, or null if the subscription does not filter by type.
Show child attributes
Show child attributes
{
"values": ["datasets_deliver_data", "materialize-view"]
}
Was this page helpful?

