Skip to main content
    Skip to content
    NorruvaDeveloper Docs
    Sandbox · verified 2026-07-29
    This page
    Whole docs
    Machine formats

    PlaygroundGet API keys
    IntroductionQuickstartAuthenticationErrors & conventionsSandbox & environments
    Entity modelProducts & categoriesPassports & versionsDigital Link & resolutionCompliance & regulationsWebhooks & eventsAutoID print loopImport jobsObservability & audit
    OverviewAuth & API keysProductsCompliance & regulationsPassportsResolver & publicWebhooksPrint jobs & devicesImport / bulkObservabilityBeyond happy path
    Integration playbookWebhook receiver guideRun a print deviceDeviations & gotchas
    EN 18222 API methodsAnnex ZA — ESPR correspondence
    Docs/API reference/Import / bulk

    Import / bulk

    Ingest many products at once with row-isolated errors and a correction loop. Concept and lifecycle: Import jobs.

    POST/api/v2/import/jobsCreate an import job
    GET/api/v2/import/jobs/{jobId}Import job status
    GET/api/v2/import/jobs/{jobId}/errorsRow-level errors
    GET/api/v2/import/jobs/{jobId}/correction-templateCorrection template
    POST/api/v2/import/jobs/{jobId}/confirmConfirm an import job
    POST/api/v2/ingest/productsMachine batch on-ramp
    GET/api/v2/ingest/{jobId}Machine on-ramp job status
    POST/api/v2/import/aasxAASX package import
    POST/api/v2/importSingle-shot CSV import
    GET/api/v2/import/templates/{regulationCode}Per-regulation template
    KEY

    Multipart CSV (UTF-8, ≤ 10 MB, up to 10,000 rows, magic-byte validated) → 202 { jobId }. Row failures are isolated — one bad row does not fail the batch.

    Parameters
    filerequiredbody · filemultipart/form-data field. UTF-8 CSV, ≤ 10 MB, ≤ 10,000 rows, magic-byte validated.
    regulationCodeoptionalbody · stringmultipart field. The FULL schema-registry code (EU-ESPR-2024-1781-ELEC, EU-BAT-2023-1542, EU-ESPR-2024-1781-TEX). Optional, but GET …/correction-template REQUIRES it: without it the job stores an empty regulation code and the correction template is a permanent 400 'no associated regulation code' no matter how many rows failed. Send it on every upload you might need to correct.
    Request
    POST {{baseUrl}}/api/v2/import/jobs
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/import/jobs" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/import/jobs`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    
    resp = requests.post(
        f"{base_url}/api/v2/import/jobs",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	url := fmt.Sprintf("%s/api/v2/import/jobs", baseURL)
    
    	req, _ := http.NewRequest("POST", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	req.Header.Set("Content-Type", "application/json")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Response

    202 Accepted → { jobId }. Re-submitting the same file is a no-op. A tenant may hold at most 3 ACTIVE (unconfirmed) jobs — the 4th is 400 "You have 3 active import jobs". Always call …/confirm; an unconfirmed job stays active, never expires, and wedges the next upload.

    Emits events
    import.completedimport.failed
    Related
    Import jobs
    KEY

    Status / preview for a job.

    Request
    GET {{baseUrl}}/api/v2/import/jobs/{jobId}
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/import/jobs/{jobId}" \
      -H "Authorization: Bearer $NORRUVA_API_KEY"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    const jobId = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/import/jobs/${jobId}`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    job_id = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/import/jobs/{job_id}",
        headers={
            "Authorization": f"Bearer {api_key}"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	jobId := "…"
    	url := fmt.Sprintf("%s/api/v2/import/jobs/%s", baseURL, jobId)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    KEY

    Row-level failures, isolated and queryable.

    Request
    GET {{baseUrl}}/api/v2/import/jobs/{jobId}/errors
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/import/jobs/{jobId}/errors" \
      -H "Authorization: Bearer $NORRUVA_API_KEY"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    const jobId = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/import/jobs/${jobId}/errors`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    job_id = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/import/jobs/{job_id}/errors",
        headers={
            "Authorization": f"Bearer {api_key}"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	jobId := "…"
    	url := fmt.Sprintf("%s/api/v2/import/jobs/%s/errors", baseURL, jobId)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    KEY

    Fixable re-upload template generated from the job's failures. Two preconditions, both 400 when unmet: the job must HAVE failures (call it only when GET …/errors is non-empty — and row failures only exist after …/confirm has run), and the job must carry a regulationCode, which is set only if you passed that multipart field at upload time.

    Request
    GET {{baseUrl}}/api/v2/import/jobs/{jobId}/correction-template
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/import/jobs/{jobId}/correction-template" \
      -H "Authorization: Bearer $NORRUVA_API_KEY"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    const jobId = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/import/jobs/${jobId}/correction-template`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    job_id = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/import/jobs/{job_id}/correction-template",
        headers={
            "Authorization": f"Bearer {api_key}"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	jobId := "…"
    	url := fmt.Sprintf("%s/api/v2/import/jobs/%s/correction-template", baseURL, jobId)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Errors you can branch on
    400
    Related
    Import jobs
    KEY

    Apply the job — drives it terminal. Always call this.

    Request
    POST {{baseUrl}}/api/v2/import/jobs/{jobId}/confirm
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/import/jobs/{jobId}/confirm" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    const jobId = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/import/jobs/${jobId}/confirm`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    job_id = "…"
    
    resp = requests.post(
        f"{base_url}/api/v2/import/jobs/{job_id}/confirm",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	jobId := "…"
    	url := fmt.Sprintf("%s/api/v2/import/jobs/%s/confirm", baseURL, jobId)
    
    	req, _ := http.NewRequest("POST", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	req.Header.Set("Content-Type", "application/json")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    KEYingest:write

    The JSON batch path TSC-style integrations use (PRD F2) — no multipart. Strict ERP-flavoured schema (unknown keys rejected); referenceId is the per-row idempotency key, so re-submitting the same rows never duplicates products.

    Parameters
    sourceSystemrequiredbody · stringe.g. "tsc-middleware".
    products[].referenceIdrequiredbody · stringPer-row identity — idempotent re-submit.
    products[].namerequiredbody · stringProduct name.
    callbackUrloptionalbody · stringOptional completion callback.
    Request
    POST {{baseUrl}}/api/v2/ingest/products
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    
    {
      "sourceSystem": "tsc-middleware",
      "products": [{
        "referenceId": "erp-4711",
        "name": "EcoCell Battery Pack",
        "description": "…", "gtin": "…",
        "category": "Electronics",
        "materials": [{ "name": "lithium", "percentage": 5.2 }],
        "manufacturing": { "facility": "Gigafactory 1", "country": "DE" },
        "customData": { "line": "A" }
      }],
      "callbackUrl": "https://integrator.example/hooks/ingest"
    }
    curl -X POST "$BASE_URL/api/v2/ingest/products" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "sourceSystem": "tsc-middleware",
      "products": [
        {
          "referenceId": "erp-4711",
          "name": "EcoCell Battery Pack",
          "description": "…",
          "gtin": "…",
          "category": "Electronics",
          "materials": [
            {
              "name": "lithium",
              "percentage": 5.2
            }
          ],
          "manufacturing": {
            "facility": "Gigafactory 1",
            "country": "DE"
          },
          "customData": {
            "line": "A"
          }
        }
      ],
      "callbackUrl": "https://integrator.example/hooks/ingest"
    }'
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/ingest/products`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`
      },
      body: JSON.stringify({
        "sourceSystem": "tsc-middleware",
        "products": [
          {
            "referenceId": "erp-4711",
            "name": "EcoCell Battery Pack",
            "description": "…",
            "gtin": "…",
            "category": "Electronics",
            "materials": [
              {
                "name": "lithium",
                "percentage": 5.2
              }
            ],
            "manufacturing": {
              "facility": "Gigafactory 1",
              "country": "DE"
            },
            "customData": {
              "line": "A"
            }
          }
        ],
        "callbackUrl": "https://integrator.example/hooks/ingest"
      })
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    
    resp = requests.post(
        f"{base_url}/api/v2/ingest/products",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json"
        },
        json={
          "sourceSystem": "tsc-middleware",
          "products": [
            {
              "referenceId": "erp-4711",
              "name": "EcoCell Battery Pack",
              "description": "…",
              "gtin": "…",
              "category": "Electronics",
              "materials": [
                {
                  "name": "lithium",
                  "percentage": 5.2
                }
              ],
              "manufacturing": {
                "facility": "Gigafactory 1",
                "country": "DE"
              },
              "customData": {
                "line": "A"
              }
            }
          ],
          "callbackUrl": "https://integrator.example/hooks/ingest"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"bytes"
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	url := fmt.Sprintf("%s/api/v2/ingest/products", baseURL)
    
    	payload := []byte(`{
      "sourceSystem": "tsc-middleware",
      "products": [
        {
          "referenceId": "erp-4711",
          "name": "EcoCell Battery Pack",
          "description": "…",
          "gtin": "…",
          "category": "Electronics",
          "materials": [
            {
              "name": "lithium",
              "percentage": 5.2
            }
          ],
          "manufacturing": {
            "facility": "Gigafactory 1",
            "country": "DE"
          },
          "customData": {
            "line": "A"
          }
        }
      ],
      "callbackUrl": "https://integrator.example/hooks/ingest"
    }`)
    	req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	req.Header.Set("Content-Type", "application/json")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Response

    202 Accepted → { jobId }. Driven green live 2026-07-22: 10,000 rows terminal in 67.5 s, 25/25 induced failures isolated row-level, idempotent re-submit with zero duplicates.

    Errors you can branch on
    400 VALIDATION_ERROR403 API_SCOPE_DENIED
    Emits events
    import.completedimport.failed
    Related
    Import jobsAutoID print loop
    KEY

    Job status / row outcomes for the machine on-ramp.

    Request
    GET {{baseUrl}}/api/v2/ingest/{jobId}
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/ingest/{jobId}" \
      -H "Authorization: Bearer $NORRUVA_API_KEY"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    const jobId = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/ingest/${jobId}`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    job_id = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/ingest/{job_id}",
        headers={
            "Authorization": f"Bearer {api_key}"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	jobId := "…"
    	url := fmt.Sprintf("%s/api/v2/ingest/%s", baseURL, jobId)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    KEY

    Multipart .aasx (OPC/ZIP) package → Draft product via the canonical create contract. XML-only payloads are refused honestly.

    Request
    POST {{baseUrl}}/api/v2/import/aasx
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/import/aasx" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/import/aasx`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    
    resp = requests.post(
        f"{base_url}/api/v2/import/aasx",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	url := fmt.Sprintf("%s/api/v2/import/aasx", baseURL)
    
    	req, _ := http.NewRequest("POST", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	req.Header.Set("Content-Type", "application/json")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    KEY

    Single-shot CSV import (≤ 50 MB).

    Request
    POST {{baseUrl}}/api/v2/import
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -X POST "$BASE_URL/api/v2/import" \
      -H "Authorization: Bearer $NORRUVA_API_KEY" \
      -H "Content-Type: application/json"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    
    const res = await fetch(`${baseUrl}/api/v2/import`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": `application/json`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    
    resp = requests.post(
        f"{base_url}/api/v2/import",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": f"application/json"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	url := fmt.Sprintf("%s/api/v2/import", baseURL)
    
    	req, _ := http.NewRequest("POST", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    	req.Header.Set("Content-Type", "application/json")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    KEY

    Per-regulation column template (same generator as category templates, keyed by regulation code). regulationCode is the FULL schema_registry code, not a short name — e.g. EU-BAT-2023-1542, EU-ESPR-2024-1781-ELEC, EU-ESPR-2024-1781-TEX. Guessing 'ESPR' returns 404. Send a key: the handler itself is declared public, but this path is NOT on the edge allowlist, so the edge middleware returns 401 to an anonymous caller before the handler runs — the same edge-gate deviation as the GS1 resolver. Any credential-shaped Bearer header passes the edge.

    Request
    GET {{baseUrl}}/api/v2/import/templates/{regulationCode}
    Authorization: Bearer {{apiKey}}
    curl -X GET "$BASE_URL/api/v2/import/templates/{regulationCode}" \
      -H "Authorization: Bearer $NORRUVA_API_KEY"
    const baseUrl = process.env.NORRUVA_BASE_URL;
    const apiKey = process.env.NORRUVA_API_KEY;
    const regulationCode = "…";
    
    const res = await fetch(`${baseUrl}/api/v2/import/templates/${regulationCode}`, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${apiKey}`
      }
    });
    
    if (!res.ok) {
      const err = await res.json();      // typed envelope: { error: { code, message, details? } }
      throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
    }
    const data = await res.json();
    import os, uuid, requests
    
    base_url = os.environ["NORRUVA_BASE_URL"]
    api_key = os.environ["NORRUVA_API_KEY"]
    regulation_code = "…"
    
    resp = requests.get(
        f"{base_url}/api/v2/import/templates/{regulation_code}",
        headers={
            "Authorization": f"Bearer {api_key}"
        },
    )
    resp.raise_for_status()   # error body is the typed envelope: {"error": {"code", "message", "details"}}
    data = resp.json()
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"os"
    )
    
    func main() {
    	baseURL := os.Getenv("NORRUVA_BASE_URL")
    	apiKey := os.Getenv("NORRUVA_API_KEY")
    	regulationCode := "…"
    	url := fmt.Sprintf("%s/api/v2/import/templates/%s", baseURL, regulationCode)
    
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Set("Authorization", "Bearer "+apiKey+"")
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		panic(err)
    	}
    	defer resp.Body.Close()
    	fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
    }
    Related
    Import jobs
    Was this page helpful?
    Thanks — noted.Feedback goes to the docs team by email.
    ← PreviousPrint jobs & devicesNext →Observability
    On this page
    POST /import/jobsGET /import/jobs/{jobId}GET /import/jobs/{jobId}/errorsGET /import/jobs/{jobId}/correction-templatePOST /import/jobs/{jobId}/confirmPOST /ingest/productsGET /ingest/{jobId}POST /import/aasxPOST /importGET /import/templates/{regulationCode}
    Norruva DPP API · sandbox developer docsGenerated 2026-07-29 · PRD-aligned (TSC roadmap rev 2) · statuses reflect E2E-verified sandbox behaviour — not marketing