Noria Pay · API reference
Payments, through one integration.
Collect, pay out and reconcile across gateways using your project's credentials and one normalized event contract.
Provider confirmation → Pay reconciles → your project receives a normalized event.
Start with an invoice
The examples use BillGrid invoice BG-INV-2026-1042 for KES 1,250.00 (125000 minor units). IDs are illustrative and credentials come from your environment. Use sandbox credentials while integrating.
Set your server environment
export NORIA_API_URL="https://pay.example.com"
export NORIA_API_KEY="<your-project-api-key>"A project key infers its workspace, application and source. Use the scopes each endpoint requires. Configuration endpoints can require a live project key even when managing sandbox accounts. Workspace administration uses dashboard sessions.
Make payment attempts durable
Persist an idempotency key before calling a money-moving endpoint. Repeating the same key and body replays the original payment; changing the body conflicts. If the outcome is unknown, reconcile the original transaction.
Read the integration quickstart →Charges
Create a charge
/v1/chargesStart a payment using the authenticated project’s configured gateway credentials. Project keys infer project ownership and source automatically; a conflicting source is refused. Callback updates commit with durable delivery jobs and wake workers immediately; scheduled status queries are a recovery fallback.
Parameters
Idempotency-Keyheader · stringrequiredRequired on every write that moves money. The same key with the same body replays the original response; the same key with a different body is rejected.
Request body
amount_minorintegerrequiredAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Greater than 0
currencystringISO 4217 currency code supported by the selected gateway account.
Default: "KES" · Maximum 3 characters
channel"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash"requiredPayment rail. The project must have a compatible configured gateway account.
providerstringGateway identifier. Credentials belong to the authenticated project.
Maximum 64 characters
mode"live" | "sandbox"Gateway environment. Test keys are restricted to sandbox payments.
referencestringrequiredYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
Maximum 64 characters
descriptionstringrequiredA readable description of this payment or integration.
Maximum 256 characters
payer_phonestringPayer's mobile number in international format, for example 254712345678.
Maximum 20 characters
payer_emailstringPayer's email. Required for a card collection.
Maximum 320 characters
payer_namestringPayer's display name.
Maximum 256 characters
return_urlstringWhere hosted card checkout should return the payer.
Maximum 2000 characters
expires_in_secondsintegerDefault: 300 · Minimum: 30
metadataobjectCaller-owned string values returned with transaction responses and normalized events.
Default: {}
sourcestringThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
Responses
201Success400Request error401Authentication required403Insufficient access409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request error502Gateway unavailable504Gateway outcome unknowncurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/charges" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: bg-invoice-1042-mpesa-attempt-1' \
--data-raw '{
"amount_minor": 125000,
"currency": "KES",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_phone": "254712345678",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-invoice-1042-mpesa-attempt-1",
},
body: JSON.stringify({
"amount_minor": 125000,
"currency": "KES",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_phone": "254712345678",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-invoice-1042-mpesa-attempt-1",
},
body: JSON.stringify({
"amount_minor": 125000,
"currency": "KES",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_phone": "254712345678",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"amount_minor": 125000,
"currency": "KES",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_phone": "254712345678",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}
request = urllib.request.Request(
base_url + "/v1/charges",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-invoice-1042-mpesa-attempt-1",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/charges');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
'Idempotency-Key: bg-invoice-1042-mpesa-attempt-1',
],
CURLOPT_POSTFIELDS => json_encode([
'amount_minor' => 125000,
'currency' => 'KES',
'channel' => 'mpesa',
'provider' => 'daraja',
'mode' => 'sandbox',
'reference' => 'BG-INV-2026-1042',
'description' => 'BillGrid invoice 1042',
'payer_phone' => '254712345678',
'payer_name' => 'Amina Otieno',
'metadata' => [
'invoice_id' => '1042',
'customer_id' => 'cust_ami_204'
]
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channel\": \"mpesa\",\n \"provider\": \"daraja\",\n \"mode\": \"sandbox\",\n \"reference\": \"BG-INV-2026-1042\",\n \"description\": \"BillGrid invoice 1042\",\n \"payer_phone\": \"254712345678\",\n \"payer_name\": \"Amina Otieno\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n }\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/charges", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "bg-invoice-1042-mpesa-attempt-1")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/charges")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "bg-invoice-1042-mpesa-attempt-1"
request.body = JSON.generate({
"amount_minor" => 125000,
"currency" => "KES",
"channel" => "mpesa",
"provider" => "daraja",
"mode" => "sandbox",
"reference" => "BG-INV-2026-1042",
"description" => "BillGrid invoice 1042",
"payer_phone" => "254712345678",
"payer_name" => "Amina Otieno",
"metadata" => {
"invoice_id" => "1042",
"customer_id" => "cust_ami_204"
}
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/charges"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "bg-invoice-1042-mpesa-attempt-1")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channel\": \"mpesa\",\n \"provider\": \"daraja\",\n \"mode\": \"sandbox\",\n \"reference\": \"BG-INV-2026-1042\",\n \"description\": \"BillGrid invoice 1042\",\n \"payer_phone\": \"254712345678\",\n \"payer_name\": \"Amina Otieno\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n }\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/charges");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Idempotency-Key", "bg-invoice-1042-mpesa-attempt-1");
request.Content = new StringContent(@"{
""amount_minor"": 125000,
""currency"": ""KES"",
""channel"": ""mpesa"",
""provider"": ""daraja"",
""mode"": ""sandbox"",
""reference"": ""BG-INV-2026-1042"",
""description"": ""BillGrid invoice 1042"",
""payer_phone"": ""254712345678"",
""payer_name"": ""Amina Otieno"",
""metadata"": {
""invoice_id"": ""1042"",
""customer_id"": ""cust_ami_204""
}
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);curl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/charges" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: bg-invoice-1042-card-attempt-1' \
--data-raw '{
"amount_minor": 125000,
"currency": "KES",
"channel": "card",
"provider": "paystack",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"payer_email": "[email protected]",
"return_url": "https://billgrid.example.com/invoices/1042/payment-result"
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-invoice-1042-card-attempt-1",
},
body: JSON.stringify({
"amount_minor": 125000,
"currency": "KES",
"channel": "card",
"provider": "paystack",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"payer_email": "[email protected]",
"return_url": "https://billgrid.example.com/invoices/1042/payment-result"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-invoice-1042-card-attempt-1",
},
body: JSON.stringify({
"amount_minor": 125000,
"currency": "KES",
"channel": "card",
"provider": "paystack",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"payer_email": "[email protected]",
"return_url": "https://billgrid.example.com/invoices/1042/payment-result"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"amount_minor": 125000,
"currency": "KES",
"channel": "card",
"provider": "paystack",
"mode": "sandbox",
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"payer_email": "[email protected]",
"return_url": "https://billgrid.example.com/invoices/1042/payment-result"
}
request = urllib.request.Request(
base_url + "/v1/charges",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-invoice-1042-card-attempt-1",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/charges');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
'Idempotency-Key: bg-invoice-1042-card-attempt-1',
],
CURLOPT_POSTFIELDS => json_encode([
'amount_minor' => 125000,
'currency' => 'KES',
'channel' => 'card',
'provider' => 'paystack',
'mode' => 'sandbox',
'reference' => 'BG-INV-2026-1042',
'description' => 'BillGrid invoice 1042',
'payer_name' => 'Amina Otieno',
'metadata' => [
'invoice_id' => '1042',
'customer_id' => 'cust_ami_204'
],
'payer_email' => '[email protected]',
'return_url' => 'https://billgrid.example.com/invoices/1042/payment-result'
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channel\": \"card\",\n \"provider\": \"paystack\",\n \"mode\": \"sandbox\",\n \"reference\": \"BG-INV-2026-1042\",\n \"description\": \"BillGrid invoice 1042\",\n \"payer_name\": \"Amina Otieno\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n },\n \"payer_email\": \"[email protected]\",\n \"return_url\": \"https://billgrid.example.com/invoices/1042/payment-result\"\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/charges", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "bg-invoice-1042-card-attempt-1")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/charges")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "bg-invoice-1042-card-attempt-1"
request.body = JSON.generate({
"amount_minor" => 125000,
"currency" => "KES",
"channel" => "card",
"provider" => "paystack",
"mode" => "sandbox",
"reference" => "BG-INV-2026-1042",
"description" => "BillGrid invoice 1042",
"payer_name" => "Amina Otieno",
"metadata" => {
"invoice_id" => "1042",
"customer_id" => "cust_ami_204"
},
"payer_email" => "[email protected]",
"return_url" => "https://billgrid.example.com/invoices/1042/payment-result"
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/charges"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "bg-invoice-1042-card-attempt-1")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channel\": \"card\",\n \"provider\": \"paystack\",\n \"mode\": \"sandbox\",\n \"reference\": \"BG-INV-2026-1042\",\n \"description\": \"BillGrid invoice 1042\",\n \"payer_name\": \"Amina Otieno\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n },\n \"payer_email\": \"[email protected]\",\n \"return_url\": \"https://billgrid.example.com/invoices/1042/payment-result\"\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/charges");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Idempotency-Key", "bg-invoice-1042-card-attempt-1");
request.Content = new StringContent(@"{
""amount_minor"": 125000,
""currency"": ""KES"",
""channel"": ""card"",
""provider"": ""paystack"",
""mode"": ""sandbox"",
""reference"": ""BG-INV-2026-1042"",
""description"": ""BillGrid invoice 1042"",
""payer_name"": ""Amina Otieno"",
""metadata"": {
""invoice_id"": ""1042"",
""customer_id"": ""cust_ami_204""
},
""payer_email"": ""[email protected]"",
""return_url"": ""https://billgrid.example.com/invoices/1042/payment-result""
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "processing",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": null,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": null,
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": null,
"failure_reason": null,
"next_action": {
"type": "await_payer",
"provider_ref": "ws_CO_20102026090000123456789012",
"payer_message": "Complete the M-Pesa prompt on your phone.",
"expires_at": "2026-10-20T09:05:00Z"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "processing",
"channel": "card",
"provider": "paystack",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": null,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": "[email protected]",
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": "bg_inv_1042_attempt_1",
"provider_receipt": null,
"failure_reason": null,
"next_action": {
"type": "redirect",
"url": "https://checkout.paystack.com/documentation-example",
"provider_ref": "bg_inv_1042_attempt_1",
"expires_at": "2026-10-20T09:05:00Z"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_unavailable",
"message": "The provider is temporarily unavailable",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "outcome_unknown",
"message": "The provider outcome is unknown; reconcile the original transaction",
"request_id": "req_docs_invoice_1042"
}
}Charges
Record a manual charge
/v1/charges/recordA cash payment, a bank transfer, a receipt read off a statement. Succeeds immediately; no provider is called.
Parameters
Idempotency-Keyheader · stringrequiredRequired on every write that moves money. The same key with the same body replays the original response; the same key with a different body is rejected.
Request body
amount_minorintegerrequiredAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Greater than 0
currencystringISO 4217 currency code supported by the selected gateway account.
Default: "KES" · Maximum 3 characters
channel"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash"requiredPayment rail. The project must have a compatible configured gateway account.
referencestringrequiredYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
Maximum 64 characters
descriptionstringrequiredA readable description of this payment or integration.
Maximum 256 characters
receiptstringUnique settlement receipt from the gateway or offline record.
Maximum 64 characters
payer_phonestringPayer's mobile number in international format, for example 254712345678.
Maximum 20 characters
payer_namestringPayer's display name.
Maximum 256 characters
paid_atstringTimestamp at which the statement or offline payment settled, in RFC 3339 format.
notestringMaximum 1024 characters
metadataobjectCaller-owned string values returned with transaction responses and normalized events.
Default: {}
sourcestringThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
Responses
201Success400Request error401Authentication required403Insufficient access409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/charges/record" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: bg-cash-receipt-1042-cash-attempt-1' \
--data-raw '{
"amount_minor": 125000,
"currency": "KES",
"channel": "cash",
"reference": "BG-INV-2026-1042",
"description": "Cash received at BillGrid office",
"receipt": "CASH-1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges/record", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-cash-receipt-1042-cash-attempt-1",
},
body: JSON.stringify({
"amount_minor": 125000,
"currency": "KES",
"channel": "cash",
"reference": "BG-INV-2026-1042",
"description": "Cash received at BillGrid office",
"receipt": "CASH-1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges/record", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-cash-receipt-1042-cash-attempt-1",
},
body: JSON.stringify({
"amount_minor": 125000,
"currency": "KES",
"channel": "cash",
"reference": "BG-INV-2026-1042",
"description": "Cash received at BillGrid office",
"receipt": "CASH-1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"amount_minor": 125000,
"currency": "KES",
"channel": "cash",
"reference": "BG-INV-2026-1042",
"description": "Cash received at BillGrid office",
"receipt": "CASH-1042",
"payer_name": "Amina Otieno",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}
request = urllib.request.Request(
base_url + "/v1/charges/record",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-cash-receipt-1042-cash-attempt-1",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/charges/record');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
'Idempotency-Key: bg-cash-receipt-1042-cash-attempt-1',
],
CURLOPT_POSTFIELDS => json_encode([
'amount_minor' => 125000,
'currency' => 'KES',
'channel' => 'cash',
'reference' => 'BG-INV-2026-1042',
'description' => 'Cash received at BillGrid office',
'receipt' => 'CASH-1042',
'payer_name' => 'Amina Otieno',
'metadata' => [
'invoice_id' => '1042',
'customer_id' => 'cust_ami_204'
]
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channel\": \"cash\",\n \"reference\": \"BG-INV-2026-1042\",\n \"description\": \"Cash received at BillGrid office\",\n \"receipt\": \"CASH-1042\",\n \"payer_name\": \"Amina Otieno\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n }\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/charges/record", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "bg-cash-receipt-1042-cash-attempt-1")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/charges/record")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "bg-cash-receipt-1042-cash-attempt-1"
request.body = JSON.generate({
"amount_minor" => 125000,
"currency" => "KES",
"channel" => "cash",
"reference" => "BG-INV-2026-1042",
"description" => "Cash received at BillGrid office",
"receipt" => "CASH-1042",
"payer_name" => "Amina Otieno",
"metadata" => {
"invoice_id" => "1042",
"customer_id" => "cust_ami_204"
}
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/charges/record"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "bg-cash-receipt-1042-cash-attempt-1")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channel\": \"cash\",\n \"reference\": \"BG-INV-2026-1042\",\n \"description\": \"Cash received at BillGrid office\",\n \"receipt\": \"CASH-1042\",\n \"payer_name\": \"Amina Otieno\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n }\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/charges/record");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Idempotency-Key", "bg-cash-receipt-1042-cash-attempt-1");
request.Content = new StringContent(@"{
""amount_minor"": 125000,
""currency"": ""KES"",
""channel"": ""cash"",
""reference"": ""BG-INV-2026-1042"",
""description"": ""Cash received at BillGrid office"",
""receipt"": ""CASH-1042"",
""payer_name"": ""Amina Otieno"",
""metadata"": {
""invoice_id"": ""1042"",
""customer_id"": ""cust_ami_204""
}
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "succeeded",
"channel": "cash",
"provider": "manual",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": 125000,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": null,
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": null,
"provider_receipt": "CASH-1042",
"failure_reason": null,
"next_action": {
"type": "none"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": "2026-10-20T09:00:18Z",
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Charges
Cancel a charge
/v1/charges/{id}/cancelRequests local cancellation of a processing charge. The provider may still take payment, so the outcome becomes unknown and remains polled until confirmed. Verified late collection settlements are recorded.
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel",
method="POST",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("POST", baseURL + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/charges/0197d1f0-21a0-7000-8000-000000000003/cancel");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "unknown",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": null,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": null,
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": null,
"failure_reason": null,
"next_action": {
"type": "none"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payouts
Create a payout
/v1/payoutsSend money to a recipient
Parameters
Idempotency-Keyheader · stringrequiredRequired on every write that moves money. The same key with the same body replays the original response; the same key with a different body is rejected.
Request body
amount_minorintegerrequiredAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Greater than 0
currencystringISO 4217 currency code supported by the selected gateway account.
Default: "KES" · Maximum 3 characters
channel"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash"requiredPayment rail. The project must have a compatible configured gateway account.
providerstringGateway identifier. Credentials belong to the authenticated project.
Maximum 64 characters
mode"live" | "sandbox"Gateway environment. Test keys are restricted to sandbox payments.
referencestringrequiredYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
Maximum 64 characters
descriptionstringrequiredA readable description of this payment or integration.
Maximum 256 characters
recipient_namestringrequiredMaximum 256 characters
recipient_phonestringRecipient's mobile number in international format.
Maximum 20 characters
recipient_accountstringRecipient's bank or gateway account identifier.
Maximum 64 characters
recipient_channel_codestringMaximum 16 characters
metadataobjectCaller-owned string values returned with transaction responses and normalized events.
Default: {}
sourcestringThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
Responses
201Success400Request error401Authentication required403Insufficient access409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request error502Gateway unavailable504Gateway outcome unknowncurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/payouts" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: bg-supplier-71-mobile-attempt-1' \
--data-raw '{
"amount_minor": 85000,
"currency": "KES",
"channel": "mpesa",
"provider": "sasapay",
"mode": "sandbox",
"reference": "BG-SUP-2026-0071",
"description": "Supplier settlement",
"recipient_name": "Njeri Supplies",
"recipient_phone": "254722345678",
"metadata": {
"supplier_id": "supplier_71"
}
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payouts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-supplier-71-mobile-attempt-1",
},
body: JSON.stringify({
"amount_minor": 85000,
"currency": "KES",
"channel": "mpesa",
"provider": "sasapay",
"mode": "sandbox",
"reference": "BG-SUP-2026-0071",
"description": "Supplier settlement",
"recipient_name": "Njeri Supplies",
"recipient_phone": "254722345678",
"metadata": {
"supplier_id": "supplier_71"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payouts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-supplier-71-mobile-attempt-1",
},
body: JSON.stringify({
"amount_minor": 85000,
"currency": "KES",
"channel": "mpesa",
"provider": "sasapay",
"mode": "sandbox",
"reference": "BG-SUP-2026-0071",
"description": "Supplier settlement",
"recipient_name": "Njeri Supplies",
"recipient_phone": "254722345678",
"metadata": {
"supplier_id": "supplier_71"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"amount_minor": 85000,
"currency": "KES",
"channel": "mpesa",
"provider": "sasapay",
"mode": "sandbox",
"reference": "BG-SUP-2026-0071",
"description": "Supplier settlement",
"recipient_name": "Njeri Supplies",
"recipient_phone": "254722345678",
"metadata": {
"supplier_id": "supplier_71"
}
}
request = urllib.request.Request(
base_url + "/v1/payouts",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-supplier-71-mobile-attempt-1",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payouts');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
'Idempotency-Key: bg-supplier-71-mobile-attempt-1',
],
CURLOPT_POSTFIELDS => json_encode([
'amount_minor' => 85000,
'currency' => 'KES',
'channel' => 'mpesa',
'provider' => 'sasapay',
'mode' => 'sandbox',
'reference' => 'BG-SUP-2026-0071',
'description' => 'Supplier settlement',
'recipient_name' => 'Njeri Supplies',
'recipient_phone' => '254722345678',
'metadata' => [
'supplier_id' => 'supplier_71'
]
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"amount_minor\": 85000,\n \"currency\": \"KES\",\n \"channel\": \"mpesa\",\n \"provider\": \"sasapay\",\n \"mode\": \"sandbox\",\n \"reference\": \"BG-SUP-2026-0071\",\n \"description\": \"Supplier settlement\",\n \"recipient_name\": \"Njeri Supplies\",\n \"recipient_phone\": \"254722345678\",\n \"metadata\": {\n \"supplier_id\": \"supplier_71\"\n }\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/payouts", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "bg-supplier-71-mobile-attempt-1")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payouts")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "bg-supplier-71-mobile-attempt-1"
request.body = JSON.generate({
"amount_minor" => 85000,
"currency" => "KES",
"channel" => "mpesa",
"provider" => "sasapay",
"mode" => "sandbox",
"reference" => "BG-SUP-2026-0071",
"description" => "Supplier settlement",
"recipient_name" => "Njeri Supplies",
"recipient_phone" => "254722345678",
"metadata" => {
"supplier_id" => "supplier_71"
}
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payouts"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "bg-supplier-71-mobile-attempt-1")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"amount_minor\": 85000,\n \"currency\": \"KES\",\n \"channel\": \"mpesa\",\n \"provider\": \"sasapay\",\n \"mode\": \"sandbox\",\n \"reference\": \"BG-SUP-2026-0071\",\n \"description\": \"Supplier settlement\",\n \"recipient_name\": \"Njeri Supplies\",\n \"recipient_phone\": \"254722345678\",\n \"metadata\": {\n \"supplier_id\": \"supplier_71\"\n }\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/payouts");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Idempotency-Key", "bg-supplier-71-mobile-attempt-1");
request.Content = new StringContent(@"{
""amount_minor"": 85000,
""currency"": ""KES"",
""channel"": ""mpesa"",
""provider"": ""sasapay"",
""mode"": ""sandbox"",
""reference"": ""BG-SUP-2026-0071"",
""description"": ""Supplier settlement"",
""recipient_name"": ""Njeri Supplies"",
""recipient_phone"": ""254722345678"",
""metadata"": {
""supplier_id"": ""supplier_71""
}
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "payout",
"status": "processing",
"channel": "mpesa",
"provider": "sasapay",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 85000,
"settled_minor": null,
"fee_minor": null,
"reference": "BG-SUP-2026-0071",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": null,
"payer_phone": null,
"payer_email": null,
"recipient_name": "Njeri Supplies",
"recipient_phone": "254722345678",
"recipient_account": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": null,
"failure_reason": null,
"next_action": {
"type": "none"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_unavailable",
"message": "The provider is temporarily unavailable",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "outcome_unknown",
"message": "The provider outcome is unknown; reconcile the original transaction",
"request_id": "req_docs_invoice_1042"
}
}Refunds
Create a refund
/v1/refundsOmit amount_minor to refund what settled. Partial refunds are allowed up to the settled amount less anything already refunded.
Parameters
Idempotency-Keyheader · stringrequiredRequired on every write that moves money. The same key with the same body replays the original response; the same key with a different body is rejected.
Request body
transaction_idstringrequiredThe original transaction's stable ID.
amount_minorintegerAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Greater than 0
reasonstringrequiredMaximum 512 characters
metadataobjectCaller-owned string values returned with transaction responses and normalized events.
Default: {}
sourcestringThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
Responses
201Success400Request error401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request error502Gateway unavailable504Gateway outcome unknowncurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/refunds" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: bg-refund-1042-partial-attempt-1' \
--data-raw '{
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"amount_minor": 25000,
"reason": "Unused service credit"
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-refund-1042-partial-attempt-1",
},
body: JSON.stringify({
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"amount_minor": 25000,
"reason": "Unused service credit"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-refund-1042-partial-attempt-1",
},
body: JSON.stringify({
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"amount_minor": 25000,
"reason": "Unused service credit"
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"amount_minor": 25000,
"reason": "Unused service credit"
}
request = urllib.request.Request(
base_url + "/v1/refunds",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "bg-refund-1042-partial-attempt-1",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/refunds');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
'Idempotency-Key: bg-refund-1042-partial-attempt-1',
],
CURLOPT_POSTFIELDS => json_encode([
'transaction_id' => '0197d1f0-21a0-7000-8000-000000000003',
'amount_minor' => 25000,
'reason' => 'Unused service credit'
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"transaction_id\": \"0197d1f0-21a0-7000-8000-000000000003\",\n \"amount_minor\": 25000,\n \"reason\": \"Unused service credit\"\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/refunds", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "bg-refund-1042-partial-attempt-1")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/refunds")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "bg-refund-1042-partial-attempt-1"
request.body = JSON.generate({
"transaction_id" => "0197d1f0-21a0-7000-8000-000000000003",
"amount_minor" => 25000,
"reason" => "Unused service credit"
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/refunds"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", "bg-refund-1042-partial-attempt-1")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"transaction_id\": \"0197d1f0-21a0-7000-8000-000000000003\",\n \"amount_minor\": 25000,\n \"reason\": \"Unused service credit\"\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/refunds");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Idempotency-Key", "bg-refund-1042-partial-attempt-1");
request.Content = new StringContent(@"{
""transaction_id"": ""0197d1f0-21a0-7000-8000-000000000003"",
""amount_minor"": 25000,
""reason"": ""Unused service credit""
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000010",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "refund",
"status": "processing",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 25000,
"settled_minor": null,
"fee_minor": null,
"reference": "BG-REFUND-1042",
"description": "Unused service credit",
"parent_transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": null,
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": null,
"failure_reason": null,
"next_action": {
"type": "none"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_unavailable",
"message": "The provider is temporarily unavailable",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "outcome_unknown",
"message": "The provider outcome is unknown; reconcile the original transaction",
"request_id": "req_docs_invoice_1042"
}
}Transactions
List transactions
/v1/transactionsList collections, payouts and refunds together
Parameters
limitquery · integercursorquery · stringdirectionquery · "collection" | "payout" | "refund"statusquery · "pending" | "processing" | "succeeded" | "failed" | "cancelled" | "expired" | "reversed" | "unknown"providerquery · stringchannelquery · "mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash"referencequery · stringreceiptquery · stringpayment_link_idquery · stringOnly transactions collected through this payment link.
parent_transaction_idquery · stringOnly refunds of this transaction.
sourcequery · string
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/transactions?limit=20&reference=BG-INV-2026-1042" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions?limit=20&reference=BG-INV-2026-1042", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions?limit=20&reference=BG-INV-2026-1042", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/transactions?limit=20&reference=BG-INV-2026-1042",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/transactions?limit=20&reference=BG-INV-2026-1042');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/transactions?limit=20&reference=BG-INV-2026-1042", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/transactions?limit=20&reference=BG-INV-2026-1042")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/transactions?limit=20&reference=BG-INV-2026-1042"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/transactions?limit=20&reference=BG-INV-2026-1042");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "succeeded",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": 125000,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": null,
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": "UJK7A4M2Q9",
"failure_reason": null,
"next_action": {
"type": "none"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": "2026-10-20T09:00:18Z",
"created_at": "2026-10-20T09:00:00Z"
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Transactions
Get a transaction
/v1/transactions/{id}Retrieve one transaction
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/transactions/0197d1f0-21a0-7000-8000-000000000003" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/transactions/0197d1f0-21a0-7000-8000-000000000003');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000003",
"object": "transaction",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "succeeded",
"channel": "mpesa",
"provider": "daraja",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": 125000,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"description": "BillGrid invoice 1042",
"parent_transaction_id": null,
"payment_link_id": null,
"payer_name": "Amina Otieno",
"payer_phone": "254712345678",
"payer_email": null,
"recipient_name": null,
"recipient_phone": null,
"recipient_account": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": "UJK7A4M2Q9",
"failure_reason": null,
"next_action": {
"type": "none"
},
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"expires_at": "2026-10-20T09:05:00Z",
"settled_at": "2026-10-20T09:00:18Z",
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Transactions
List transaction events
/v1/transactions/{id}/eventsThe full audit trail, including every raw provider exchange
Parameters
idpath · stringrequiredlimitquery · integercursorquery · string
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/transactions/0197d1f0-21a0-7000-8000-000000000003/events");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000007",
"object": "transaction.event",
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"type": "succeeded",
"sequence": 3,
"payload": {
"provider_receipt": "UJK7A4M2Q9"
},
"occurred_at": "2026-10-20T09:00:18Z"
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
List gateway accounts
/v1/payment-methodsList configured providers; credentials are never returned
Parameters
limitquery · integercursorquery · stringinclude_retiredquery · booleanInclude historical credential versions for statement account selection.
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/payment-methods?include_retired=true" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods?include_retired=true", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods?include_retired=true", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-methods?include_retired=true",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-methods?include_retired=true');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/payment-methods?include_retired=true", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-methods?include_retired=true")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-methods?include_retired=true"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/payment-methods?include_retired=true");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000004",
"object": "payment_method",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"provider": "daraja",
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": true,
"configured": true,
"config_hint": {
"shortcode": "174379",
"consumer_key": "…demo",
"consumer_secret": "…demo",
"passkey": "…demo"
},
"callback_url": "https://pay.example.com/hooks/ra/7GkN2pVbR4tHc9WdZ",
"last_verified_at": "2026-10-20T09:00:00Z",
"retired_at": null,
"created_at": "2026-10-20T09:00:00Z"
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
Configure gateway credentials
/v1/payment-methods/{provider}Credentials are sealed at rest and never read back. The response carries the callback URL to register with the provider.
Parameters
providerpath · stringrequired
Request body
labelstringrequiredMaximum 128 characters
mode"live" | "sandbox"Gateway environment. Test keys are restricted to sandbox payments.
Default: "live"
channelsarray<"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash">requiredenabledbooleanWhether this account or endpoint accepts new work. Paused webhook endpoints retain queued events.
Default: true
configProviderConfigrequiredChild fields
M-Pesa Daraja
certificatestringMaximum 8192 characters
consumer_keystringrequiredMaximum 256 characters
consumer_secretstringrequiredMaximum 256 characters
initiator_namestringMaximum 64 characters
initiator_passwordstringMaximum 256 characters
passkeystringrequiredMaximum 256 characters
provider"daraja"requiredGateway identifier. Credentials belong to the authenticated project.
shortcodestringrequiredtransaction_type"CustomerPayBillOnline" | "CustomerBuyGoodsOnline"Default: "CustomerPayBillOnline"
KCB Buni
api_keystringrequiredMaximum 256 characters
company_codestringMaximum 15 characters
consumer_keystringrequiredMaximum 256 characters
consumer_secretstringrequiredMaximum 256 characters
debit_accountstringMaximum 10 characters
ipn_public_keystringrequiredMaximum 4096 characters
pass_keystringrequiredMaximum 256 characters
provider"kcb_buni"requiredGateway identifier. Credentials belong to the authenticated project.
route_codestringrequiredMaximum 32 characters
shared_short_codebooleanDefault: false
short_codestringrequired
Paystack
provider"paystack"requiredGateway identifier. Credentials belong to the authenticated project.
public_keystringMaximum 256 characters
secret_keystringrequiredMaximum 256 characters
SasaPay
client_idstringrequiredMaximum 256 characters
client_secretstringrequiredMaximum 256 characters
merchant_codestringrequiredMaximum 32 characters
provider"sasapay"requiredGateway identifier. Credentials belong to the authenticated project.
Responses
200Success400Request error401Authentication required403Insufficient access409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X PUT "$NORIA_API_URL/v1/payment-methods/daraja" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": true,
"config": {
"provider": "daraja",
"shortcode": "174379",
"consumer_key": "replace_with_sandbox_consumer_key",
"consumer_secret": "replace_with_sandbox_consumer_secret",
"passkey": "replace_with_sandbox_passkey"
}
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja", {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": true,
"config": {
"provider": "daraja",
"shortcode": "174379",
"consumer_key": "replace_with_sandbox_consumer_key",
"consumer_secret": "replace_with_sandbox_consumer_secret",
"passkey": "replace_with_sandbox_passkey"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja", {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": true,
"config": {
"provider": "daraja",
"shortcode": "174379",
"consumer_key": "replace_with_sandbox_consumer_key",
"consumer_secret": "replace_with_sandbox_consumer_secret",
"passkey": "replace_with_sandbox_passkey"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": True,
"config": {
"provider": "daraja",
"shortcode": "174379",
"consumer_key": "replace_with_sandbox_consumer_key",
"consumer_secret": "replace_with_sandbox_consumer_secret",
"passkey": "replace_with_sandbox_passkey"
}
}
request = urllib.request.Request(
base_url + "/v1/payment-methods/daraja",
method="PUT",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-methods/daraja');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'mode' => 'sandbox',
'label' => 'BillGrid M-Pesa sandbox',
'channels' => [
'mpesa'
],
'enabled' => true,
'config' => [
'provider' => 'daraja',
'shortcode' => '174379',
'consumer_key' => 'replace_with_sandbox_consumer_key',
'consumer_secret' => 'replace_with_sandbox_consumer_secret',
'passkey' => 'replace_with_sandbox_passkey'
]
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"mode\": \"sandbox\",\n \"label\": \"BillGrid M-Pesa sandbox\",\n \"channels\": [\n \"mpesa\"\n ],\n \"enabled\": true,\n \"config\": {\n \"provider\": \"daraja\",\n \"shortcode\": \"174379\",\n \"consumer_key\": \"replace_with_sandbox_consumer_key\",\n \"consumer_secret\": \"replace_with_sandbox_consumer_secret\",\n \"passkey\": \"replace_with_sandbox_passkey\"\n }\n}")
req, err := http.NewRequest("PUT", baseURL + "/v1/payment-methods/daraja", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-methods/daraja")
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"mode" => "sandbox",
"label" => "BillGrid M-Pesa sandbox",
"channels" => [
"mpesa"
],
"enabled" => true,
"config" => {
"provider" => "daraja",
"shortcode" => "174379",
"consumer_key" => "replace_with_sandbox_consumer_key",
"consumer_secret" => "replace_with_sandbox_consumer_secret",
"passkey" => "replace_with_sandbox_passkey"
}
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-methods/daraja"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString("{\n \"mode\": \"sandbox\",\n \"label\": \"BillGrid M-Pesa sandbox\",\n \"channels\": [\n \"mpesa\"\n ],\n \"enabled\": true,\n \"config\": {\n \"provider\": \"daraja\",\n \"shortcode\": \"174379\",\n \"consumer_key\": \"replace_with_sandbox_consumer_key\",\n \"consumer_secret\": \"replace_with_sandbox_consumer_secret\",\n \"passkey\": \"replace_with_sandbox_passkey\"\n }\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + "/v1/payment-methods/daraja");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""mode"": ""sandbox"",
""label"": ""BillGrid M-Pesa sandbox"",
""channels"": [
""mpesa""
],
""enabled"": true,
""config"": {
""provider"": ""daraja"",
""shortcode"": ""174379"",
""consumer_key"": ""replace_with_sandbox_consumer_key"",
""consumer_secret"": ""replace_with_sandbox_consumer_secret"",
""passkey"": ""replace_with_sandbox_passkey""
}
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000004",
"object": "payment_method",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"provider": "daraja",
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": true,
"configured": true,
"config_hint": {
"shortcode": "174379",
"consumer_key": "…demo",
"consumer_secret": "…demo",
"passkey": "…demo"
},
"callback_url": "https://pay.example.com/hooks/ra/7GkN2pVbR4tHc9WdZ",
"last_verified_at": "2026-10-20T09:00:00Z",
"retired_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
Enable or pause an account
/v1/payment-methods/{provider}Keeps the credentials. A provider that is off takes no new payments; one already under way still settles.
Parameters
providerpath · stringrequiredmodequery · "live" | "sandbox"Select the exact environment for inspection or mutation.
Request body
enabledbooleanrequiredWhether this account or endpoint accepts new work. Paused webhook endpoints retain queued events.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X PATCH "$NORIA_API_URL/v1/payment-methods/daraja" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"enabled": false
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabled": false
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabled": false
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"enabled": False
}
request = urllib.request.Request(
base_url + "/v1/payment-methods/daraja",
method="PATCH",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-methods/daraja');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'enabled' => false
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"enabled\": false\n}")
req, err := http.NewRequest("PATCH", baseURL + "/v1/payment-methods/daraja", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-methods/daraja")
request = Net::HTTP::Patch.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"enabled" => false
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-methods/daraja"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString("{\n \"enabled\": false\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + "/v1/payment-methods/daraja");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""enabled"": false
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000004",
"object": "payment_method",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"provider": "daraja",
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": false,
"configured": true,
"config_hint": {
"shortcode": "174379",
"consumer_key": "…demo",
"consumer_secret": "…demo",
"passkey": "…demo"
},
"callback_url": "https://pay.example.com/hooks/ra/7GkN2pVbR4tHc9WdZ",
"last_verified_at": "2026-10-20T09:00:00Z",
"retired_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
Retire a gateway account
/v1/payment-methods/{provider}Remove a provider
Parameters
providerpath · stringrequiredmodequery · "live" | "sandbox"Select the exact environment for inspection or mutation.
Request
This endpoint has no JSON request body.
Responses
204Removed401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X DELETE "$NORIA_API_URL/v1/payment-methods/daraja?mode=sandbox" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja?mode=sandbox", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja?mode=sandbox", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-methods/daraja?mode=sandbox",
method="DELETE",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-methods/daraja?mode=sandbox');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("DELETE", baseURL + "/v1/payment-methods/daraja?mode=sandbox", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-methods/daraja?mode=sandbox")
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-methods/daraja?mode=sandbox"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + "/v1/payment-methods/daraja?mode=sandbox");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);No response body.
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
Verify credentials
/v1/payment-methods/{provider}/verifyProve the credentials work against the provider
Parameters
providerpath · stringrequiredmodequery · "live" | "sandbox"Select the exact environment for inspection or mutation.
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request error502Gateway unavailablecurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/payment-methods/daraja/verify?mode=sandbox" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja/verify?mode=sandbox", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/daraja/verify?mode=sandbox", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-methods/daraja/verify?mode=sandbox",
method="POST",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-methods/daraja/verify?mode=sandbox');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("POST", baseURL + "/v1/payment-methods/daraja/verify?mode=sandbox", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-methods/daraja/verify?mode=sandbox")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-methods/daraja/verify?mode=sandbox"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/payment-methods/daraja/verify?mode=sandbox");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000004",
"object": "payment_method",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"provider": "daraja",
"mode": "sandbox",
"label": "BillGrid M-Pesa sandbox",
"channels": [
"mpesa"
],
"enabled": true,
"configured": true,
"config_hint": {
"shortcode": "174379",
"consumer_key": "…demo",
"consumer_secret": "…demo",
"passkey": "…demo"
},
"callback_url": "https://pay.example.com/hooks/ra/7GkN2pVbR4tHc9WdZ",
"last_verified_at": "2026-10-20T09:00:00Z",
"retired_at": null,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_unavailable",
"message": "The provider is temporarily unavailable",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
Get account balance
/v1/payment-methods/{provider}/balanceRetrieve balances from the selected project's configured gateway account. Availability depends on the provider; this does not move money.
Parameters
providerpath · stringrequiredmodequery · "live" | "sandbox"Select the exact environment for inspection or mutation.
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request error502Gateway unavailablecurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/payment-methods/paystack/balance?mode=sandbox" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/paystack/balance?mode=sandbox", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-methods/paystack/balance?mode=sandbox", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-methods/paystack/balance?mode=sandbox",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-methods/paystack/balance?mode=sandbox');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/payment-methods/paystack/balance?mode=sandbox", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-methods/paystack/balance?mode=sandbox")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-methods/paystack/balance?mode=sandbox"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/payment-methods/paystack/balance?mode=sandbox");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "balance",
"provider": "paystack",
"mode": "sandbox",
"available": [
{
"currency": "KES",
"amount_minor": 4200000
}
],
"retrieved_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_unavailable",
"message": "The provider is temporarily unavailable",
"request_id": "req_docs_invoice_1042"
}
}Payment links
Create a payment link
/v1/payment-linksCreate a hosted checkout link
Request body
titlestringrequiredMaximum 256 characters
descriptionstringA readable description of this payment or integration.
Maximum 1024 characters
currencystringISO 4217 currency code supported by the selected gateway account.
Default: "KES" · Maximum 3 characters
amount_minorintegerAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Greater than 0
min_amount_minorintegerGreater than 0
max_amount_minorintegerGreater than 0
allow_custom_amountbooleanDefault: false
channelsarray<"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash">Default: []
referencestringYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
Maximum 64 characters
return_urlstringWhere hosted card checkout should return the payer.
Maximum 2000 characters
activebooleanDefault: true
metadataobjectCaller-owned string values returned with transaction responses and normalized events.
Default: {}
sourcestringThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
Responses
201Success400Request error401Authentication required403Insufficient access409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/payment-links" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"amount_minor": 125000,
"currency": "KES",
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"return_url": "https://billgrid.example.com/invoices/1042",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"amount_minor": 125000,
"currency": "KES",
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"return_url": "https://billgrid.example.com/invoices/1042",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"amount_minor": 125000,
"currency": "KES",
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"return_url": "https://billgrid.example.com/invoices/1042",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"amount_minor": 125000,
"currency": "KES",
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"return_url": "https://billgrid.example.com/invoices/1042",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
}
}
request = urllib.request.Request(
base_url + "/v1/payment-links",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-links');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'title' => 'BillGrid invoice 1042',
'description' => 'October service subscription',
'amount_minor' => 125000,
'currency' => 'KES',
'channels' => [
'mpesa',
'card'
],
'reference' => 'BG-INV-2026-1042',
'return_url' => 'https://billgrid.example.com/invoices/1042',
'metadata' => [
'invoice_id' => '1042',
'customer_id' => 'cust_ami_204'
]
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"title\": \"BillGrid invoice 1042\",\n \"description\": \"October service subscription\",\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channels\": [\n \"mpesa\",\n \"card\"\n ],\n \"reference\": \"BG-INV-2026-1042\",\n \"return_url\": \"https://billgrid.example.com/invoices/1042\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n }\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/payment-links", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-links")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"title" => "BillGrid invoice 1042",
"description" => "October service subscription",
"amount_minor" => 125000,
"currency" => "KES",
"channels" => [
"mpesa",
"card"
],
"reference" => "BG-INV-2026-1042",
"return_url" => "https://billgrid.example.com/invoices/1042",
"metadata" => {
"invoice_id" => "1042",
"customer_id" => "cust_ami_204"
}
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-links"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"title\": \"BillGrid invoice 1042\",\n \"description\": \"October service subscription\",\n \"amount_minor\": 125000,\n \"currency\": \"KES\",\n \"channels\": [\n \"mpesa\",\n \"card\"\n ],\n \"reference\": \"BG-INV-2026-1042\",\n \"return_url\": \"https://billgrid.example.com/invoices/1042\",\n \"metadata\": {\n \"invoice_id\": \"1042\",\n \"customer_id\": \"cust_ami_204\"\n }\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/payment-links");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""title"": ""BillGrid invoice 1042"",
""description"": ""October service subscription"",
""amount_minor"": 125000,
""currency"": ""KES"",
""channels"": [
""mpesa"",
""card""
],
""reference"": ""BG-INV-2026-1042"",
""return_url"": ""https://billgrid.example.com/invoices/1042"",
""metadata"": {
""invoice_id"": ""1042"",
""customer_id"": ""cust_ami_204""
}
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000005",
"object": "payment_link",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"token": "pl_billgrid_invoice_1042_example",
"url": "https://pay.example.com/p/pl_billgrid_invoice_1042_example",
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"currency": "KES",
"amount_minor": 125000,
"min_amount_minor": null,
"max_amount_minor": null,
"allow_custom_amount": false,
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"active": true,
"created_at": "2026-10-20T09:00:00Z",
"paid_count": 3,
"collected_minor": 375000
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment links
List payment links
/v1/payment-linksList payment links
Parameters
limitquery · integercursorquery · string
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/payment-links" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-links",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-links');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/payment-links", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-links")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-links"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/payment-links");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000005",
"object": "payment_link",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"token": "pl_billgrid_invoice_1042_example",
"url": "https://pay.example.com/p/pl_billgrid_invoice_1042_example",
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"currency": "KES",
"amount_minor": 125000,
"min_amount_minor": null,
"max_amount_minor": null,
"allow_custom_amount": false,
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"active": true,
"created_at": "2026-10-20T09:00:00Z",
"paid_count": 3,
"collected_minor": 375000
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment links
Get a payment link
/v1/payment-links/{id}Retrieve a payment link
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000005",
"object": "payment_link",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"token": "pl_billgrid_invoice_1042_example",
"url": "https://pay.example.com/p/pl_billgrid_invoice_1042_example",
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"currency": "KES",
"amount_minor": 125000,
"min_amount_minor": null,
"max_amount_minor": null,
"allow_custom_amount": false,
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"active": true,
"created_at": "2026-10-20T09:00:00Z",
"paid_count": 3,
"collected_minor": 375000
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment links
Update a payment link
/v1/payment-links/{id}Update a payment link
Parameters
idpath · stringrequired
Request body
titlestringMaximum 256 characters
descriptionstring | nullA readable description of this payment or integration.
Child fields
Variant 1
Variant 2
currencystringISO 4217 currency code supported by the selected gateway account.
Maximum 3 characters
amount_minorinteger | nullAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Child fields
Variant 1
Variant 2
min_amount_minorinteger | nullChild fields
Variant 1
Variant 2
max_amount_minorinteger | nullChild fields
Variant 1
Variant 2
allow_custom_amountbooleanchannelsarray<"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash">referencestring | nullYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
Child fields
Variant 1
Variant 2
return_urlstring | nullWhere hosted card checkout should return the payer.
Child fields
Variant 1
Variant 2
activebooleanmetadataobjectCaller-owned string values returned with transaction responses and normalized events.
sourcestring | nullThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
Responses
200Success400Request error401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X PATCH "$NORIA_API_URL/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"active": false
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"active": false
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"active": false
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"active": False
}
request = urllib.request.Request(
base_url + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005",
method="PATCH",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'active' => false
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"active\": false\n}")
req, err := http.NewRequest("PATCH", baseURL + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005")
request = Net::HTTP::Patch.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"active" => false
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString("{\n \"active\": false\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""active"": false
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000005",
"object": "payment_link",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"token": "pl_billgrid_invoice_1042_example",
"url": "https://pay.example.com/p/pl_billgrid_invoice_1042_example",
"title": "BillGrid invoice 1042",
"description": "October service subscription",
"currency": "KES",
"amount_minor": 125000,
"min_amount_minor": null,
"max_amount_minor": null,
"allow_custom_amount": false,
"channels": [
"mpesa",
"card"
],
"reference": "BG-INV-2026-1042",
"active": false,
"created_at": "2026-10-20T09:00:00Z",
"paid_count": 3,
"collected_minor": 375000
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment links
Close a payment link
/v1/payment-links/{id}Close a payment link
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
204Closed401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X DELETE "$NORIA_API_URL/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005",
method="DELETE",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("DELETE", baseURL + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005")
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + "/v1/payment-links/0197d1f0-21a0-7000-8000-000000000005");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);No response body.
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment links
Payment link analytics
/v1/payment-links/{id}/analyticsReturns payment attempts, outcomes, settled totals and the last 30 days of successful volume for one link.
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/payment-links/undefined/analytics" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/undefined/analytics", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/payment-links/undefined/analytics", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/payment-links/undefined/analytics",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/payment-links/undefined/analytics');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/payment-links/undefined/analytics", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/payment-links/undefined/analytics")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/payment-links/undefined/analytics"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/payment-links/undefined/analytics");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "payment_link.analytics",
"payment_link_id": "0197d1f0-21a0-7000-8000-000000000005",
"currency": "KES",
"attempts": 2,
"succeeded": 1,
"pending": 0,
"failed": 1,
"unknown": 0,
"gross_minor": 125000,
"settled_minor": 125000,
"fees_minor": 0,
"average_minor": 125000,
"last_payment_at": "2026-10-20T09:00:18Z",
"daily": [
{
"day": "2026-10-20",
"count": 1,
"amount_minor": 125000
}
]
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Statements
Import a statement
/v1/statementsImport a statement for the selected project and payment_method_id (required for independent projects). Match only that gateway account’s existing charges. A statement never creates a payment. Duplicate imports replay the stored result.
Request body
source"mpesa" | "bank" | "sasapay" | "paystack" | "other"requiredThe project slug, set by Pay from the project.
labelstringrequiredMaximum 256 characters
currencystringISO 4217 currency code supported by the selected gateway account.
Default: "KES" · Maximum 3 characters
rowsarray<object>requiredChild fields
receiptstringrequiredUnique settlement receipt from the gateway or offline record.
Maximum 64 characters
amount_minorintegerrequiredAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
Greater than 0
paid_atstringrequiredTimestamp at which the statement or offline payment settled, in RFC 3339 format.
referencestringYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
Maximum 64 characters
payer_phonestringPayer's mobile number in international format, for example 254712345678.
Maximum 20 characters
payer_namestringPayer's display name.
Maximum 256 characters
narrativestringMaximum 512 characters
payment_method_idstringThe gateway credential version recorded on the transaction. Required when importing an independent project's statement.
Responses
201Success400Request error401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/statements" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"paid_at": "2026-10-20T09:00:18Z",
"reference": "BG-INV-2026-1042",
"payer_phone": "254712345678"
}
]
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/statements", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"paid_at": "2026-10-20T09:00:18Z",
"reference": "BG-INV-2026-1042",
"payer_phone": "254712345678"
}
]
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/statements", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"paid_at": "2026-10-20T09:00:18Z",
"reference": "BG-INV-2026-1042",
"payer_phone": "254712345678"
}
]
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"paid_at": "2026-10-20T09:00:18Z",
"reference": "BG-INV-2026-1042",
"payer_phone": "254712345678"
}
]
}
request = urllib.request.Request(
base_url + "/v1/statements",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/statements');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'payment_method_id' => '0197d1f0-21a0-7000-8000-000000000004',
'source' => 'mpesa',
'label' => 'M-Pesa settlement · 20 October',
'currency' => 'KES',
'rows' => [
[
'receipt' => 'UJK7A4M2Q9',
'amount_minor' => 125000,
'paid_at' => '2026-10-20T09:00:18Z',
'reference' => 'BG-INV-2026-1042',
'payer_phone' => '254712345678'
]
]
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"payment_method_id\": \"0197d1f0-21a0-7000-8000-000000000004\",\n \"source\": \"mpesa\",\n \"label\": \"M-Pesa settlement · 20 October\",\n \"currency\": \"KES\",\n \"rows\": [\n {\n \"receipt\": \"UJK7A4M2Q9\",\n \"amount_minor\": 125000,\n \"paid_at\": \"2026-10-20T09:00:18Z\",\n \"reference\": \"BG-INV-2026-1042\",\n \"payer_phone\": \"254712345678\"\n }\n ]\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/statements", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/statements")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"payment_method_id" => "0197d1f0-21a0-7000-8000-000000000004",
"source" => "mpesa",
"label" => "M-Pesa settlement · 20 October",
"currency" => "KES",
"rows" => [
{
"receipt" => "UJK7A4M2Q9",
"amount_minor" => 125000,
"paid_at" => "2026-10-20T09:00:18Z",
"reference" => "BG-INV-2026-1042",
"payer_phone" => "254712345678"
}
]
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/statements"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"payment_method_id\": \"0197d1f0-21a0-7000-8000-000000000004\",\n \"source\": \"mpesa\",\n \"label\": \"M-Pesa settlement · 20 October\",\n \"currency\": \"KES\",\n \"rows\": [\n {\n \"receipt\": \"UJK7A4M2Q9\",\n \"amount_minor\": 125000,\n \"paid_at\": \"2026-10-20T09:00:18Z\",\n \"reference\": \"BG-INV-2026-1042\",\n \"payer_phone\": \"254712345678\"\n }\n ]\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/statements");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""payment_method_id"": ""0197d1f0-21a0-7000-8000-000000000004"",
""source"": ""mpesa"",
""label"": ""M-Pesa settlement · 20 October"",
""currency"": ""KES"",
""rows"": [
{
""receipt"": ""UJK7A4M2Q9"",
""amount_minor"": 125000,
""paid_at"": ""2026-10-20T09:00:18Z"",
""reference"": ""BG-INV-2026-1042"",
""payer_phone"": ""254712345678""
}
]
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000009",
"object": "statement_import",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"row_count": 1,
"settled": 1,
"duplicates": 0,
"unmatched": 0,
"ambiguous": 0,
"settled_minor": 125000,
"unmatched_minor": 0,
"replayed": false,
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"outcome": "settled",
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"reference": "BG-INV-2026-1042",
"detail": "Matched and settled"
}
],
"created_at": "2026-10-20T10:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Statements
List statement imports
/v1/statementsList statement imports
Parameters
limitquery · integercursorquery · string
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/statements" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/statements", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/statements", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/statements",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/statements');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/statements", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/statements")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/statements"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/statements");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000009",
"object": "statement_import",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"row_count": 1,
"settled": 1,
"duplicates": 0,
"unmatched": 0,
"ambiguous": 0,
"settled_minor": 125000,
"unmatched_minor": 0,
"replayed": false,
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"outcome": "settled",
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"reference": "BG-INV-2026-1042",
"detail": "Matched and settled"
}
],
"created_at": "2026-10-20T10:00:00Z"
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Statements
Get statement results
/v1/statements/{id}One import and what came of every row
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/statements/0197d1f0-21a0-7000-8000-000000000009" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/statements/0197d1f0-21a0-7000-8000-000000000009');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/statements/0197d1f0-21a0-7000-8000-000000000009");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000009",
"object": "statement_import",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"payment_method_id": "0197d1f0-21a0-7000-8000-000000000004",
"source": "mpesa",
"label": "M-Pesa settlement · 20 October",
"currency": "KES",
"row_count": 1,
"settled": 1,
"duplicates": 0,
"unmatched": 0,
"ambiguous": 0,
"settled_minor": 125000,
"unmatched_minor": 0,
"replayed": false,
"rows": [
{
"receipt": "UJK7A4M2Q9",
"amount_minor": 125000,
"outcome": "settled",
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"reference": "BG-INV-2026-1042",
"detail": "Matched and settled"
}
],
"created_at": "2026-10-20T10:00:00Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
Create a webhook endpoint
/v1/webhook-endpointsRegister a settlement webhook; the secret is shown once
Request body
urlstringrequiredMaximum 2000 characters
descriptionstringA readable description of this payment or integration.
Maximum 500 characters
event_typesarray<"created" | "dispatched" | "callback_received" | "queried" | "succeeded" | "failed" | "cancelled" | "expired" | "outcome_unknown" | "reversed">Normalized event subscriptions. An empty list subscribes to all event types.
Default: []
enabledbooleanWhether this account or endpoint accepts new work. Paused webhook endpoints retain queued events.
Default: true
sourcestring | nullThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
include_provider_payloadbooleanInclude redacted gateway callback data alongside normalized payment fields.
Responses
201Success400Request error401Authentication required403Insufficient access409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/webhook-endpoints" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"include_provider_payload": false
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"include_provider_payload": false
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"include_provider_payload": false
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"include_provider_payload": False
}
request = urllib.request.Request(
base_url + "/v1/webhook-endpoints",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-endpoints');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://billgrid.example.com/webhooks/noria-pay',
'description' => 'Invoice reconciliation',
'event_types' => [
'succeeded',
'failed',
'outcome_unknown',
'reversed'
],
'include_provider_payload' => false
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"url\": \"https://billgrid.example.com/webhooks/noria-pay\",\n \"description\": \"Invoice reconciliation\",\n \"event_types\": [\n \"succeeded\",\n \"failed\",\n \"outcome_unknown\",\n \"reversed\"\n ],\n \"include_provider_payload\": false\n}")
req, err := http.NewRequest("POST", baseURL + "/v1/webhook-endpoints", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-endpoints")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"url" => "https://billgrid.example.com/webhooks/noria-pay",
"description" => "Invoice reconciliation",
"event_types" => [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"include_provider_payload" => false
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-endpoints"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"url\": \"https://billgrid.example.com/webhooks/noria-pay\",\n \"description\": \"Invoice reconciliation\",\n \"event_types\": [\n \"succeeded\",\n \"failed\",\n \"outcome_unknown\",\n \"reversed\"\n ],\n \"include_provider_payload\": false\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/webhook-endpoints");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""url"": ""https://billgrid.example.com/webhooks/noria-pay"",
""description"": ""Invoice reconciliation"",
""event_types"": [
""succeeded"",
""failed"",
""outcome_unknown"",
""reversed""
],
""include_provider_payload"": false
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000006",
"object": "webhook_endpoint",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"enabled": true,
"include_provider_payload": false,
"created_at": "2026-10-20T09:00:00Z",
"secret": "whsec_documentation_example_store_yours_once"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
List webhook endpoints
/v1/webhook-endpointsList webhook endpoints
Parameters
limitquery · integercursorquery · string
Request
This endpoint has no JSON request body.
Responses
200Success401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/webhook-endpoints" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/webhook-endpoints",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-endpoints');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/webhook-endpoints", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-endpoints")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-endpoints"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/webhook-endpoints");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000006",
"object": "webhook_endpoint",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"enabled": true,
"include_provider_payload": false,
"created_at": "2026-10-20T09:00:00Z"
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
Update a webhook endpoint
/v1/webhook-endpoints/{id}Update a webhook endpoint; what it omits is left alone
Parameters
idpath · stringrequired
Request body
urlstringMaximum 2000 characters
descriptionstringA readable description of this payment or integration.
Maximum 500 characters
event_typesarray<"created" | "dispatched" | "callback_received" | "queried" | "succeeded" | "failed" | "cancelled" | "expired" | "outcome_unknown" | "reversed">Normalized event subscriptions. An empty list subscribes to all event types.
enabledbooleanWhether this account or endpoint accepts new work. Paused webhook endpoints retain queued events.
sourcestring | nullThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
include_provider_payloadbooleanInclude redacted gateway callback data alongside normalized payment fields.
Responses
200Success400Request error401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X PATCH "$NORIA_API_URL/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data-raw '{
"enabled": true
}'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabled": true
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabled": true
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
payload = {
"enabled": True
}
request = urllib.request.Request(
base_url + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006",
method="PATCH",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'enabled' => true
], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
body := strings.NewReader("{\n \"enabled\": true\n}")
req, err := http.NewRequest("PATCH", baseURL + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006", body)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006")
request = Net::HTTP::Patch.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
"enabled" => true
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString("{\n \"enabled\": true\n}"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
request.Content = new StringContent(@"{
""enabled"": true
}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000006",
"object": "webhook_endpoint",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"url": "https://billgrid.example.com/webhooks/noria-pay",
"description": "Invoice reconciliation",
"event_types": [
"succeeded",
"failed",
"outcome_unknown",
"reversed"
],
"enabled": true,
"include_provider_payload": false,
"created_at": "2026-10-20T09:00:00Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
Delete a webhook endpoint
/v1/webhook-endpoints/{id}Delete a webhook endpoint
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
204Deleted401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X DELETE "$NORIA_API_URL/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006",
method="DELETE",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("DELETE", baseURL + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006")
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + "/v1/webhook-endpoints/0197d1f0-21a0-7000-8000-000000000006");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);No response body.
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
List bank codes
/v1/channel-codesList cached payout bank codes
Request
This endpoint has no JSON request body.
Responses
200Bank channel codes401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request error503Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/channel-codes" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/channel-codes", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/channel-codes", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/channel-codes",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/channel-codes');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/channel-codes", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/channel-codes")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/channel-codes"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/channel-codes");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "channel_codes",
"bank": [
{
"code": "BillGrid invoice 1042",
"name": "BillGrid"
}
],
"source": "billgrid",
"fetched_at": "2026-10-20T09:00:18Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Payment methods
Refresh bank codes
/v1/channel-codes/refreshRefresh the cached payout bank codes from SasaPay
Request
This endpoint has no JSON request body.
Responses
200Refreshed bank channel codes401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request error502Gateway unavailable503Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/channel-codes/refresh" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/channel-codes/refresh", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/channel-codes/refresh", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/channel-codes/refresh",
method="POST",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/channel-codes/refresh');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("POST", baseURL + "/v1/channel-codes/refresh", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/channel-codes/refresh")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/channel-codes/refresh"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/channel-codes/refresh");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "channel_codes",
"bank": [
{
"code": "BillGrid invoice 1042",
"name": "BillGrid"
}
],
"source": "billgrid",
"fetched_at": "2026-10-20T09:00:18Z"
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_unavailable",
"message": "The provider is temporarily unavailable",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}API reference
List supported gateways
/v1/providersList registered provider adapters and setup metadata
Request
This endpoint has no JSON request body.
Responses
200Provider catalog401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/providers" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/providers", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/providers", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/providers",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/providers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/providers", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/providers")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/providers"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/providers");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"provider": "daraja",
"name": "BillGrid",
"configurable": true,
"channels": [
"BillGrid invoice 1042"
],
"modes": [
"BillGrid invoice 1042"
],
"currencies": [
"BillGrid invoice 1042"
],
"fields": [
{
"name": "BillGrid",
"label": "BillGrid"
}
],
"capabilities": {
"collections": [
"BillGrid invoice 1042"
],
"payouts": [
"BillGrid invoice 1042"
],
"refunds": true,
"status": true,
"balances": true
},
"config_schema": {
"type": "object",
"properties": {},
"required": [
"BillGrid invoice 1042"
],
"x-property-order": [
"BillGrid invoice 1042"
]
},
"confirm_unsigned_collections": true,
"icon": "BillGrid invoice 1042",
"description": "Invoice collections for BillGrid",
"priority": {}
}
],
"next_cursor": null
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Transactions
Payment activity summary
/v1/transactions/summaryUnresolved counts and complete 14-day settlement totals for the authenticated project, or authorized workspace when no project is selected.
Request
This endpoint has no JSON request body.
Responses
200Workspace payment summary401Authentication required403Insufficient access409Request conflicts with existing state429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/transactions/summary" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions/summary", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/transactions/summary", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/transactions/summary",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/transactions/summary');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/transactions/summary", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/transactions/summary")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/transactions/summary"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/transactions/summary");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "transaction_summary",
"currency": "KES",
"period_days": 14,
"unknown": 0,
"waiting": 1,
"daily": [
{
"day": "2026-10-20",
"collected": 125000,
"paid_out": 0
}
]
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
List webhook deliveries
/v1/webhook-deliveriesInspect project webhook delivery and retry status
Parameters
limitquery · integercursorquery · stringstatusquery · "pending" | "delivering" | "delivered" | "failed"endpoint_idquery · string
Request
This endpoint has no JSON request body.
Responses
200Success400Request error401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/webhook-deliveries?status=pending&limit=20" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-deliveries?status=pending&limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-deliveries?status=pending&limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/webhook-deliveries?status=pending&limit=20",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-deliveries?status=pending&limit=20');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/webhook-deliveries?status=pending&limit=20", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-deliveries?status=pending&limit=20")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-deliveries?status=pending&limit=20"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/webhook-deliveries?status=pending&limit=20");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"object": "list",
"data": [
{
"id": "0197d1f0-21a0-7000-8000-000000000008",
"object": "webhook_delivery",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"endpoint_id": "0197d1f0-21a0-7000-8000-000000000006",
"endpoint_url": "https://billgrid.example.com/webhooks/noria-pay",
"endpoint_enabled": true,
"event_id": "0197d1f0-21a0-7000-8000-000000000007",
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"event_type": "succeeded",
"status": "pending",
"attempts": 1,
"max_attempts": 8,
"response_status": 503,
"last_error": "Webhook delivery failed",
"next_attempt_at": "2026-10-20T09:00:23Z",
"delivered_at": null,
"created_at": "2026-10-20T09:00:18Z"
}
],
"next_cursor": null
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
Get a delivery
/v1/webhook-deliveries/{id}One delivery of an event to an endpoint, with its attempts, last response and next retry
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
200Success400Request error401Authentication required403Insufficient access404Resource not found429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X GET "$NORIA_API_URL/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008",
method="GET",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("GET", baseURL + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000008",
"object": "webhook_delivery",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"endpoint_id": "0197d1f0-21a0-7000-8000-000000000006",
"endpoint_url": "https://billgrid.example.com/webhooks/noria-pay",
"endpoint_enabled": true,
"event_id": "0197d1f0-21a0-7000-8000-000000000007",
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"event_type": "succeeded",
"status": "pending",
"attempts": 1,
"max_attempts": 8,
"response_status": 503,
"last_error": "Webhook delivery failed",
"next_attempt_at": "2026-10-20T09:00:23Z",
"delivered_at": null,
"created_at": "2026-10-20T09:00:18Z"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Webhooks
Retry a delivery
/v1/webhook-deliveries/{id}/retryQueue a pending or failed event again without redispatching its payment
Parameters
idpath · stringrequired
Request
This endpoint has no JSON request body.
Responses
200Success400Request error401Authentication required403Insufficient access404Resource not found409Request conflicts with existing state422Validation or gateway configuration error429Rate limited500Request errorcurl --fail-with-body --max-time 30 -X POST "$NORIA_API_URL/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry" \
-H "Authorization: Bearer $NORIA_API_KEY" \
-H 'Accept: application/json'const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);const baseUrl = process.env.NORIA_API_URL ?? "https://pay.example.com";
const response = await fetch(baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORIA_API_KEY}`,
"Accept": "application/json",
},
});
const text = await response.text();
if (!response.ok) throw new Error(`Noria Pay ${response.status}: ${text}`);
const result = text ? JSON.parse(text) : null;
console.log(result);import json
import os
import urllib.error
import urllib.request
base_url = os.environ.get("NORIA_API_URL", "https://pay.example.com")
request = urllib.request.Request(
base_url + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry",
method="POST",
headers={
"Authorization": "Bearer " + os.environ["NORIA_API_KEY"],
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
print(json.loads(raw) if raw else None)
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error<?php
$baseUrl = getenv('NORIA_API_URL') ?: 'https://pay.example.com';
$ch = curl_init($baseUrl . '/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NORIA_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException($raw);
$result = $raw !== '' ? json_decode($raw, true, 512, JSON_THROW_ON_ERROR) : null;
print_r($result);package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
baseURL := os.Getenv("NORIA_API_URL")
if baseURL == "" { baseURL = "https://pay.example.com" }
req, err := http.NewRequest("POST", baseURL + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry", nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("NORIA_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 400 { panic(string(data)) }
fmt.Println(string(data))
}require "json"
require "net/http"
require "uri"
uri = URI(ENV.fetch("NORIA_API_URL", "https://pay.example.com") + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NORIA_API_KEY')}"
request["Accept"] = "application/json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
http.request(request)
end
raise "Noria Pay #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
result = response.body.to_s.empty? ? nil : JSON.parse(response.body)
puts resultimport java.net.URI;
import java.net.http.*;
import java.time.Duration;
class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("NORIA_API_URL", "https://pay.example.com");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("NORIA_API_KEY"))
.header("Accept", "application/json")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
}using System;
using System.Net.Http;
using System.Text;
var baseUrl = Environment.GetEnvironmentVariable("NORIA_API_URL") ?? "https://pay.example.com";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + "/v1/webhook-deliveries/0197d1f0-21a0-7000-8000-000000000008/retry");
request.Headers.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("NORIA_API_KEY"));
request.Headers.Add("Accept", "application/json");
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) throw new Exception(raw);
Console.WriteLine(raw);{
"id": "0197d1f0-21a0-7000-8000-000000000008",
"object": "webhook_delivery",
"status": "pending"
}{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "forbidden",
"message": "This key cannot select another project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "not_found",
"message": "Transaction not found",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "idempotency_mismatch",
"message": "This idempotency key was already used with a different request",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "provider_not_configured",
"message": "Configure a compatible gateway account for this project",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests; wait before retrying",
"request_id": "req_docs_invoice_1042"
}
}{
"error": {
"code": "internal_error",
"message": "An internal error occurred",
"request_id": "req_docs_invoice_1042"
}
}Push updates
Normalized payment events
One event contract across gateways, delivered to the originating project.
Reconcile an invoice
- Verify
Pay-Signatureagainst the exact raw request bytes and check timestamp tolerance. - Persist the event and acknowledge with
2xxpromptly. - Deduplicate by event ID. Keep the newest sequence for each transaction.
- Apply the payment credit idempotently by transaction ID. A later success can add a receipt without adding another credit.
Only a settled outcome confirms payment. created, dispatched and callback_received do not.
Pay commits reconciliation and the durable delivery job before waking workers. Polling recovers missed provider confirmations. Delivery is at least once and can arrive out of order.
Payload fields
idstringrequiredtype"created" | "dispatched" | "callback_received" | "queried" | "succeeded" | "failed" | "cancelled" | "expired" | "outcome_unknown" | "reversed"requiredcreated_atstringrequireddataobjectrequiredChild fields
transaction_idstring | nullrequiredThe original transaction's stable ID.
workspace_idstringrequiredWorkspace containing the owner project.
direction"collection" | "payout" | "refund"requiredstatus"pending" | "processing" | "succeeded" | "failed" | "cancelled" | "expired" | "reversed" | "unknown"requiredproviderstringrequiredGateway identifier. Credentials belong to the authenticated project.
Maximum 64 characters
channel"mpesa" | "airtel" | "card" | "bank" | "wallet" | "cash"requiredPayment rail. The project must have a compatible configured gateway account.
currencystringrequiredISO 4217 currency code supported by the selected gateway account.
amount_minorintegerrequiredAmount in integer minor units. KES 1,250.00 is 125000; Kenyan mobile-money rails require whole shillings.
settled_minorinteger | nullrequiredConfirmed settlement in minor units, or null before confirmation.
Child fields
Variant 1
Variant 2
underpaidbooleanrequiredfee_minorinteger | nullrequiredGateway fee in minor units when available.
Child fields
Variant 1
Variant 2
referencestringrequiredYour business reference, such as an invoice number. Reuse the original payment attempt's idempotency key for safe retries.
provider_receiptstring | nullrequiredGateway settlement receipt, which can be added by a later reconciliation event.
failure_reasonstring | nullrequiredmetadataobjectrequiredCaller-owned string values returned with transaction responses and normalized events.
detailobjectrequiredsourcestring | nullThe project slug. Pay sets it from the project; a different value is refused.
Maximum 64 characters
mode"live" | "sandbox"Gateway environment. Test keys are restricted to sandbox payments.
parent_transaction_idstring | nullpayment_link_idstring | nullprovider_refstring | nullGateway correlation reference; it is not proof of settlement.
settled_atstring | nullprovider_payloadobjectOptional redacted gateway callback, included only when enabled on the destination.
application_idstringImmutable owner project ID. A project API key infers this automatically.
sequenceintegerIncreasing sequence within a transaction. Deduplicate by event id and ignore older sequences.
Minimum: 0
{
"id": "0197d1f0-21a0-7000-8000-000000000007",
"type": "succeeded",
"sequence": 3,
"created_at": "2026-10-20T09:00:18Z",
"data": {
"transaction_id": "0197d1f0-21a0-7000-8000-000000000003",
"workspace_id": "0197d1f0-21a0-7000-8000-000000000001",
"application_id": "0197d1f0-21a0-7000-8000-000000000002",
"source": "billgrid",
"direction": "collection",
"status": "succeeded",
"provider": "daraja",
"channel": "mpesa",
"mode": "sandbox",
"currency": "KES",
"amount_minor": 125000,
"settled_minor": 125000,
"underpaid": false,
"fee_minor": null,
"reference": "BG-INV-2026-1042",
"parent_transaction_id": null,
"payment_link_id": null,
"provider_ref": "ws_CO_20102026090000123456789012",
"provider_receipt": "UJK7A4M2Q9",
"failure_reason": null,
"settled_at": "2026-10-20T09:00:18Z",
"metadata": {
"invoice_id": "1042",
"customer_id": "cust_ami_204"
},
"detail": {
"provider_receipt": "UJK7A4M2Q9"
}
}
}