API reference
Public x402 image uploads, response fields, and runnable order recovery examples.
Base URL: https://api.naven.network/v1/ipfs.
| Method | Path | Purpose |
|---|---|---|
| GET | /config | Availability, price, limits, and retention |
| POST | /upload | Multipart file; 402 quote, then payment and image upload |
| GET | /orders/{id} | Query status using the original Idempotency-Key |
| POST | /orders/{id}/reconcile | Reconcile a previously verified pending payment |
For dependencies, wallet setup, and a complete upload script, start with the quickstart.
Authentication and idempotency
Every endpoint except /config requires Idempotency-Key, a randomly generated UUID.
It identifies and authorizes one upload order. No account, project authentication,
or Naven API key is required.
- Generate the key once with
crypto.randomUUID()orrandomUUIDfromnode:crypto. - Save it privately before sending the first upload request.
- Reuse it with the same image bytes and media type when retrying. A different image requires a different key; changing the image under an existing key returns 409.
- Save the order ID from the x402 challenge's
resource.urlquery parameterorderId. The successful response also includes the ID asdata.id. - Keep the key out of public URLs, source control, and logs. It grants access to the order.
The optional orderId query parameter on /upload must match the order resolved by
the key. The payment payload must preserve the resource URL from the challenge,
including its order ID.
Check availability
GET /config is a free request and needs no headers:
curl https://api.naven.network/v1/ipfs/configThe response uses the standard envelope { "code": 0, "message": "ok", "data": ... }.
The data object includes:
| Field | Meaning |
|---|---|
available | Whether the upload service is ready to accept requests |
upload.priceUsd | Display price as a string; currently "0.005" |
upload.maxFileBytes | Maximum image size: 5000000 |
upload.maxPixels | Maximum width × height: 16000000 |
upload.mimeTypes | image/jpeg, image/png, and image/webp |
upload.retentionDays | Included storage period: 365 |
delivery.included | Image delivery is included |
delivery.renewalsAvailable | Whether renewals are available; currently false |
An unavailable service can still return HTTP 200 from /config; check
data.available before uploading. The payable network, token, recipient, and exact
atomic amount come from the upload's 402 challenge.
Upload an image
POST /upload accepts multipart/form-data with exactly one file field.
| Input | Required | Description |
|---|---|---|
Idempotency-Key header | Yes | Saved UUID for this upload order |
file form field | Yes | Static JPEG, PNG, or WebP image |
PAYMENT-SIGNATURE header | For payment | x402 v2 payment encoded by your client |
orderId query parameter | No | Order ID, if supplied, must match the saved key |
Images must be non-empty, at most 5,000,000 bytes, and at most 16 megapixels.
Animated images are rejected. The entire multipart request must fit within
5,100,000 bytes. Let FormData or your HTTP library set Content-Type and its boundary.
Payment challenge
The first valid unpaid upload returns HTTP 402 with a base64-encoded
PAYMENT-REQUIRED header and the x402 challenge in the JSON body. This is the expected
payment step, not a completed upload. The challenge is an x402 object, not a Naven
data envelope.
| Challenge field | Meaning |
|---|---|
x402Version | Protocol version: 2 |
resource.url | Canonical upload URL with ?orderId=...; bind the payment to this exact URL |
accepts[0].scheme | Payment scheme: exact |
accepts[0].network | CAIP-2 network identifier; currently eip155:4663 |
accepts[0].asset | USDG token contract address on the requested network |
accepts[0].amount | Exact amount as an atomic-unit string; currently "5000" (0.005 USDG) |
accepts[0].payTo | Recipient of this upload payment |
accepts[0].extra | Token signing metadata used by the EVM client |
Use the x402 SDK to sign the challenge and encode PAYMENT-SIGNATURE. Save the
encoded payment before sending it, then repeat the multipart upload with the
same key and image. A plain token transfer does not replace the signed x402 request.
Successful response
A completed upload returns HTTP 200. PAYMENT-RESPONSE contains the x402
settlement result after confirmed payment. The JSON body has this shape; angle-bracket
values below stand for the real values returned for your image:
{
"code": 0,
"message": "ok",
"data": {
"id": "f7bcd3e4-0d89-4f7f-9d65-c109a4882170",
"status": "fulfilled",
"amountAtomic": "5000",
"retentionDays": 365,
"transactionHash": "<payment-transaction-hash>",
"quoteExpiresAt": "<ISO-8601-timestamp>",
"expiresAt": "<ISO-8601-timestamp>",
"paymentUrl": "https://api.naven.network/v1/ipfs/upload?orderId=f7bcd3e4-0d89-4f7f-9d65-c109a4882170",
"cid": "<image-cid>",
"ipfsUri": "ipfs://<image-cid>",
"url": "https://assets.naven.network/ipfs/<image-cid>"
}
}| Response field | Description |
|---|---|
id | Upload order ID; use it together with the original key for recovery |
status | Current order state; see the table below |
amountAtomic | Payment amount in the token's smallest units, returned as a string |
retentionDays | Included storage duration |
transactionHash | Confirmed payment transaction hash, or null before confirmation |
quoteExpiresAt | Expiry of the unpaid quote; this is not the storage expiry |
expiresAt | End of the included storage period, or null before upload completion |
paymentUrl | Canonical payment resource URL for this order |
cid, ipfsUri, url | Public image identifiers and URL, returned only when status is fulfilled; otherwise null |
Always use the returned url for sharing or embedding instead of constructing a
URL from an assumed host. Pending orders do not publish partial storage URLs.
Query an order
GET /orders/{id} takes the original Idempotency-Key header and returns the same
order fields inside data. It does not create a payment or require a wallet signature.
A missing order or a key that does not authorize that order returns 404.
| Status | Meaning and next action |
|---|---|
awaiting_payment | Payment has not been confirmed. Continue the saved upload with its original quote and any saved authorization. |
payment_pending | A verified payment is awaiting confirmation. Query or reconcile it; do not sign another payment. |
paid | Payment is confirmed but storage is incomplete. Repeat the upload with the original file and key; no new signature is needed. |
fulfilled | Upload is complete. Use the returned image URL, CID, and expiry date. |
expired | The stored image's retention has ended. Public image fields are no longer returned. |
Unpaid quotes last one hour. Check quoteExpiresAt as well as status: an unpaid
order can still report awaiting_payment after its quote has expired, and attempting
to pay it returns 410. Paid orders remain redeemable; their storage year begins when
the upload succeeds.
Recover a payment
POST /orders/{id}/reconcile accepts JSON and the original key:
{ "transactionHash": "0x<64-hex-character-payment-transaction-hash>" }Use the transaction hash of the payment associated with this order. Reconciliation only confirms a previously verified pending payment; it does not accept an unrelated transfer, charge your wallet, or upload the image. One payment authorization or transaction cannot fund two orders.
Save the following as recover.ts beside the quickstart script.
It reads the saved key and order ID, checks the order, and optionally submits a
transaction hash for reconciliation. It uses Bun's built-in APIs and needs no wallet
private key or additional imports beyond the one shown:
import { readFile } from "node:fs/promises";
const api = "https://api.naven.network/v1/ipfs";
async function readData(response: Response) {
const body = await response.json();
if (!response.ok || body.code !== 0) {
throw new Error(`HTTP ${response.status}: ${body.message ?? "Request failed"}`);
}
return body.data;
}
async function main() {
const [statePath, transactionHash] = process.argv.slice(2);
if (!statePath) {
throw new Error("Usage: bun run recover.ts ./image.png.naven-upload.json [transactionHash]");
}
const { key, orderId } = JSON.parse(await readFile(statePath, "utf8"));
if (!key || !orderId) throw new Error("The saved upload has no order ID yet");
if (transactionHash && !/^0x[0-9a-f]{64}$/i.test(transactionHash)) {
throw new Error("Provide a 0x-prefixed, 64-hex-character transaction hash");
}
const orderUrl = `${api}/orders/${encodeURIComponent(orderId)}`;
let order = await readData(await fetch(orderUrl, {
headers: { "Idempotency-Key": key },
}));
if (transactionHash && order.status === "payment_pending") {
order = await readData(await fetch(`${orderUrl}/reconcile`, {
method: "POST",
headers: { "Idempotency-Key": key, "Content-Type": "application/json" },
body: JSON.stringify({ transactionHash }),
}));
}
console.log(JSON.stringify(order, null, 2));
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : "Recovery failed");
process.exitCode = 1;
});Check the order without submitting a transaction:
bun run recover.ts ./image.png.naven-upload.jsonIf it is payment_pending and you have the payment's transaction hash, pass the
actual hash in place of 0xYOUR_PAYMENT_TRANSACTION_HASH:
bun run recover.ts ./image.png.naven-upload.json 0xYOUR_PAYMENT_TRANSACTION_HASHIf the payment cannot yet be confirmed, reconciliation returns 409 and the order remains pending. Query again or retry reconciliation later without sending a new payment. If you do not have a transaction hash, keep the saved state and check the order again; a request timeout alone does not mean payment failed.
Once the order becomes paid, finish storing the image with the original command:
bun run upload.ts ./image.pngErrors and retries
Errors normally use { "code": <number>, "message": "...", "data": null }.
The initial 402 challenge is the exception described above. Always check the HTTP
status before reading success fields.
| HTTP status | Meaning | Next action |
|---|---|---|
| 400 | Invalid key, body, payment proof, or file field | Correct the request; preserve an existing order's key and file |
| 402 | Payment required or verification failed | Handle a valid initial challenge; for a failed saved payment, inspect the order before signing again |
| 404 | Order not found for this key | Check that both the ID and key come from the same saved upload |
| 409 | Changed file, upload in progress, or uncertain payment | Restore the original image, wait for the active upload, or inspect/reconcile the order as indicated by the message |
| 410 | Unpaid quote expired | Create a new order only after confirming the old one was unpaid with no uncertain settlement |
| 413 | File or multipart request exceeds its limit | Use a smaller image for a new upload |
| 415 | Unsupported/invalid image, animation, or more than 16 MP | Use a supported static image within the limits |
| 502 | Upload request or storage failed | Inspect the saved order and retry with the same key and image; a confirmed paid order is not charged again |
| 503 | Upload service temporarily unavailable | Check /config and retry later with the saved state |
Do not generate a new key or sign another payment to bypass a pending order. There are no bandwidth purchase, project management, per-image pause, or renewal endpoints.