curl --request POST \
--url https://api.numi.sh/v1/account/attribution \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"source": "hn",
"medium": "post",
"campaign": "launch",
"referrer_host": "news.ycombinator.com",
"landing_path": "/companies/msft",
"first_seen": "2026-10-30"
}
'import requests
url = "https://api.numi.sh/v1/account/attribution"
payload = {
"source": "hn",
"medium": "post",
"campaign": "launch",
"referrer_host": "news.ycombinator.com",
"landing_path": "/companies/msft",
"first_seen": "2026-10-30"
}
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({
source: 'hn',
medium: 'post',
campaign: 'launch',
referrer_host: 'news.ycombinator.com',
landing_path: '/companies/msft',
first_seen: '2026-10-30'
})
};
fetch('https://api.numi.sh/v1/account/attribution', 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.numi.sh/v1/account/attribution",
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([
'source' => 'hn',
'medium' => 'post',
'campaign' => 'launch',
'referrer_host' => 'news.ycombinator.com',
'landing_path' => '/companies/msft',
'first_seen' => '2026-10-30'
]),
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.numi.sh/v1/account/attribution"
payload := strings.NewReader("{\n \"source\": \"hn\",\n \"medium\": \"post\",\n \"campaign\": \"launch\",\n \"referrer_host\": \"news.ycombinator.com\",\n \"landing_path\": \"/companies/msft\",\n \"first_seen\": \"2026-10-30\"\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.numi.sh/v1/account/attribution")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"source\": \"hn\",\n \"medium\": \"post\",\n \"campaign\": \"launch\",\n \"referrer_host\": \"news.ycombinator.com\",\n \"landing_path\": \"/companies/msft\",\n \"first_seen\": \"2026-10-30\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.numi.sh/v1/account/attribution")
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 \"source\": \"hn\",\n \"medium\": \"post\",\n \"campaign\": \"launch\",\n \"referrer_host\": \"news.ycombinator.com\",\n \"landing_path\": \"/companies/msft\",\n \"first_seen\": \"2026-10-30\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"recorded": true
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA",
"as_of": "2026-10-19T12:34:56.789Z"
}
}{
"error": {
"code": "bad_request",
"message": "limit: must be from 1 to 50"
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}{
"error": {
"code": "unauthorized",
"message": "Sign in to numi to manage your account."
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}{
"error": {
"code": "rate_limit_exceeded",
"message": "The sandbox plan has a limit of 20 requests a minute. Retry in 12 seconds.",
"retry_after": 12,
"scope": "minute"
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}{
"error": {
"code": "service_unavailable",
"message": "Accounts are not configured on this server, so you cannot sign in here."
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}Record where the account first came from
Records once, for the signed-in account, the first-touch attribution that the numi.sh site saved in the browser before sign-up, such as the campaign tag of the link and the domain of the site that linked to numi.sh. The first call records, and every later call changes nothing and answers recorded as false. A field that does not follow its rule is stored as null, and the call still succeeds. The site sends this call by itself, so you need it only if you build your own client of the web app.
curl --request POST \
--url https://api.numi.sh/v1/account/attribution \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"source": "hn",
"medium": "post",
"campaign": "launch",
"referrer_host": "news.ycombinator.com",
"landing_path": "/companies/msft",
"first_seen": "2026-10-30"
}
'import requests
url = "https://api.numi.sh/v1/account/attribution"
payload = {
"source": "hn",
"medium": "post",
"campaign": "launch",
"referrer_host": "news.ycombinator.com",
"landing_path": "/companies/msft",
"first_seen": "2026-10-30"
}
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({
source: 'hn',
medium: 'post',
campaign: 'launch',
referrer_host: 'news.ycombinator.com',
landing_path: '/companies/msft',
first_seen: '2026-10-30'
})
};
fetch('https://api.numi.sh/v1/account/attribution', 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.numi.sh/v1/account/attribution",
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([
'source' => 'hn',
'medium' => 'post',
'campaign' => 'launch',
'referrer_host' => 'news.ycombinator.com',
'landing_path' => '/companies/msft',
'first_seen' => '2026-10-30'
]),
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.numi.sh/v1/account/attribution"
payload := strings.NewReader("{\n \"source\": \"hn\",\n \"medium\": \"post\",\n \"campaign\": \"launch\",\n \"referrer_host\": \"news.ycombinator.com\",\n \"landing_path\": \"/companies/msft\",\n \"first_seen\": \"2026-10-30\"\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.numi.sh/v1/account/attribution")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"source\": \"hn\",\n \"medium\": \"post\",\n \"campaign\": \"launch\",\n \"referrer_host\": \"news.ycombinator.com\",\n \"landing_path\": \"/companies/msft\",\n \"first_seen\": \"2026-10-30\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.numi.sh/v1/account/attribution")
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 \"source\": \"hn\",\n \"medium\": \"post\",\n \"campaign\": \"launch\",\n \"referrer_host\": \"news.ycombinator.com\",\n \"landing_path\": \"/companies/msft\",\n \"first_seen\": \"2026-10-30\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"recorded": true
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA",
"as_of": "2026-10-19T12:34:56.789Z"
}
}{
"error": {
"code": "bad_request",
"message": "limit: must be from 1 to 50"
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}{
"error": {
"code": "unauthorized",
"message": "Sign in to numi to manage your account."
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}{
"error": {
"code": "rate_limit_exceeded",
"message": "The sandbox plan has a limit of 20 requests a minute. Retry in 12 seconds.",
"retry_after": 12,
"scope": "minute"
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}{
"error": {
"code": "service_unavailable",
"message": "Accounts are not configured on this server, so you cannot sign in here."
},
"meta": {
"request_id": "req_Qm3xYb9Kd2LpR7wA"
}
}Authorizations
A session of the numi web app, sent as Authorization: Bearer <token>. Only the account routes take it.
Body
The campaign source, from the utm_source tag. It is trimmed and lower-cased, and it holds 1 to 64 characters of a-z, 0-9, dot, underscore and hyphen. Any other value is stored as null.
"hn"
The campaign medium, from the utm_medium tag, under the same rule as source.
"post"
The campaign name, from the utm_campaign tag, under the same rule as source.
"launch"
The host name of the site that linked to numi.sh, with no scheme, path or port. It holds 1 to 253 characters of a-z, 0-9, dot and hyphen, with at least one dot. Any other value is stored as null.
"news.ycombinator.com"
The path of the first page opened. It starts with a slash, holds at most 255 characters, and has no query or fragment. Any other value is stored as null.
"/companies/msft"
The day of the first visit, as YYYY-MM-DD. It must be a day on the calendar from 2026-10-01 to tomorrow in UTC. Any other value is stored as null.
"2026-10-30"