@ontemper/edi
v1.3.0
Published
An EDI client with structured logging.
Readme
Temper EDI Client
A TypeScript EDI client with structured logging and distributed tracing capabilities, built for processing X12 and UN/EDIFACT documents with comprehensive error handling and automatic span tracking.
Features
- X12 EDI Processing: Complete X12 document parsing, validation, and generation
- UN/EDIFACT Processing: Parse, validate, acknowledge (CONTRL), and generate EDIFACT interchanges
- Structured Logging: Built-in integration with Temper Logger for comprehensive operation tracking
- Distributed Tracing: Automatic span creation for all EDI operations with detailed payload information
- Type Safety: Full TypeScript support with comprehensive type definitions
- Error Handling: Custom error types with detailed error codes and context
Installation
npm install @ontemper/edi
# or
bun add @ontemper/edi
# or
yarn add @ontemper/ediEnvironment Variables
TemperEdiClient is a thin HTTP client for Temper's EDI service, and it configures itself entirely from the environment — the platform injects the right values per environment, the same way it injects UNNBOUND_API_URL, so you don't set these by hand.
UNNBOUND_EDI_BASE_URL— where the EDI service is reachable from this environment. A deployed workflow runs inside the cluster and gets the service's internal URL directly (no credential — cluster networking is the trust boundary). The builder sandbox runs outside the cluster, so it gets Temper's authenticated/api/edi-gatewayproxy instead.UNNBOUND_EDI_API_KEY— the credential for whichever path needs one: a platform-minted JWT through the sandbox gateway, or an EDINation-cloud key in the legacy no-base-URL setup. The direct in-cluster path needs no key and ignores it.
The client throws only if it finds neither. There's nothing to provision, hardcode, or gate on — just new TemperEdiClient().
Quick Start
import { TemperEdiClient } from '@ontemper/edi';
import { logger } from 'unnbound-logger-sdk';
// In a Temper workflow environment the EDI connection is already configured
// via env (platform-injected) — just construct the client:
const edi = new TemperEdiClient();
// Parse X12 document to JSON
const x12Interchanges = await edi.fromX12({
input: 'ISA*00* *00* *12*NOTP...',
});
// Validate X12 interchange
const validationResult = await edi.validateX12({
input: x12Interchanges[0],
});
// Convert JSON back to X12
const x12Document = await edi.toX12({
input: x12Interchanges[0],
});
// Generate and serialize acknowledgments
const acknowledgments = await edi.acknowledgeX12({
input: x12Interchanges[0],
});
for (const acknowledgment of acknowledgments) {
// Apply partner-specific outbound envelope IDs here when needed.
const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
}API Reference
Constructor
new TemperEdiClient(); // platform-injected env
new TemperEdiClient({ token }); // caller-supplied gateway credentialCreates a new EDI client instance. It configures itself from two platform-owned environment variables — UNNBOUND_EDI_BASE_URL (where Temper's EDI service is reachable from this environment: the in-cluster URL in a deployed workflow, the /api/edi-gateway proxy in the builder sandbox, or the public gateway origin in environments that cannot reach the cluster) and UNNBOUND_EDI_API_KEY (the credential for whichever path needs one — a gateway JWT, or a legacy EDINation-cloud key; the direct in-cluster path ignores it). In a Temper workflow environment the platform injects the right pair. Throws only if neither is set.
A workflow that authenticates on its own behalf passes the run's invocation token as token. The client presents it as the gateway credential (Ocp-Apim-Subscription-Key) and as the Authorization: Bearer credential for control-number allocation, so concurrent runs never share a credential and the process environment stays untouched. An explicit UNNBOUND_EDI_API_KEY always wins, which keeps platform-injected gateway JWTs and customer EDINation keys authoritative.
Methods
fromX12(options: FromX12Options): Promise<X12Interchange[]>
Parses X12 EDI documents into JSON format.
const x12Interchanges = await edi.fromX12({
input: 'ISA*00* *00* *12*NOTP...',
});Parameters:
options.input- The X12 document as a string or File object
Returns: Array of parsed X12 interchange objects
toX12(options: ToX12Options): Promise<string>
Converts JSON X12 interchange objects back to X12 format. Automatically stamps control numbers (ISA13, GS06, ST02) and their matching trailers if not already set.
const x12Document = await edi.toX12({
input: x12InterchangeObject,
});Parameters:
options.input- The X12 interchange object to convert
Returns: X12 document as string
Control number auto-stamping: Leave InterchangeControlNumber_13, GroupControlNumber_6, and TransactionSetControlNumber_02 empty or omit them. Empty and all-zero ISA13, GS06, or ST02 values are treated as unassigned. Valid non-zero values are preserved.
validateX12(options: ValidateX12Options): Promise<OperationResult>
Validates X12 interchange objects against EDI standards.
const validationResult = await edi.validateX12({
input: x12InterchangeObject,
});Parameters:
options.input- The X12 interchange object to validate
Returns: Validation result with operation details
acknowledgeX12(options: AcknolwedgeX12Options): Promise<X12Interchange[]>
Generates acknowledgment interchange objects. Apply any partner-specific outbound envelope IDs, then serialize
each acknowledgment with toX12(). During serialization, toX12() assigns control numbers using the final
sender and receiver. The method always returns an array because one input can produce more than one
acknowledgment.
const acknowledgments = await edi.acknowledgeX12({
input: x12InterchangeObject,
});
for (const acknowledgment of acknowledgments) {
// Apply partner-specific outbound envelope IDs here when needed.
const serialized = await edi.toX12({ input: acknowledgment });
}Parameters:
options.input- The X12 interchange object to acknowledge
Returns: The generated acknowledgment interchanges
UN/EDIFACT
fromEdifact, toEdifact, validateEdifact, and acknowledgeEdifact take the same options as their X12
counterparts and call the service's /edifact/* routes. They use the same errors and error codes.
const [interchange] = await edi.fromEdifact({ input: "UNA:+.? 'UNB+UNOA:2+SENDER+RECEIVER+961101:1200+1'…" });
const verdict = await edi.validateEdifact({ input: interchange });
const contrl = await edi.acknowledgeEdifact({ input: interchange }); // [] when no CONTRL is due
const edifact = await edi.toEdifact({ input: interchange });toEdifact does not stamp control numbers: set the interchange reference (UNB 0020,
UNB.InterchangeControlReference_5, and the matching UNZ) and each message reference (UNH 0062) before you
serialize. This also applies to each CONTRL that acknowledgeEdifact returns: the service fills in the same
reference every time, and a partner can refuse a repeated one. The service loads the message model (for example
D.96A ORDERS) on first use.
Error Handling
The EDI client throws two distinct error classes. The distinction matters for file routing:
a TemperEdiClientError is a verdict on the input document (bad file — don't retry) or a
persistent authentication failure, while an EdiInfrastructureError is retryable and does
not indicate that the document is invalid (error.retryable === true).
import { isEdiInfrastructureError, isTemperEdiClientError } from '@ontemper/edi';
async function processFile(rawX12: string) {
try {
return await edi.fromX12({ input: rawX12 });
} catch (error) {
if (isEdiInfrastructureError(error)) {
// Keep the file and rethrow so the caller can retry.
throw error;
}
if (isTemperEdiClientError(error)) {
if (error.code === 'edi_unauthorized_error') {
// Surface authentication failures instead of quarantining the file.
throw error;
}
// A verdict on the document (see codes below) — handle it terminally (e.g.
// quarantine the file) and STOP. Do not rethrow into retry paths.
console.error('EDI Error:', error.code, error.message);
return undefined;
}
throw error; // unrecognized — surface to the caller
}
}Error Codes
TemperEdiClientError (verdict on the document):
| Code | Description |
| ------------------------ | ------------------------------------------- |
| edi_read_error | Error parsing an X12 or EDIFACT document |
| edi_write_error | Error converting JSON to X12 or EDIFACT |
| edi_validate_error | Error validating an X12 or EDIFACT document |
| edi_acknowledge_error | Error generating acknowledgment |
| edi_unauthorized_error | Unauthorized access to EDI service |
| edi_unknown_error | Legacy — no longer produced |
EdiInfrastructureError (retryable: true):
| Code | Description |
| ---------------------- | ------------------------------------------------- |
| infrastructure_error | Retryable failure unrelated to the input document |
Type Definitions
FromX12Options
interface FromX12Options {
input: unknown; // X12 document as string or File
}ToX12Options
interface ToX12Options {
input: X12Interchange; // X12 interchange object
}ValidateX12Options
interface ValidateX12Options {
input: X12Interchange; // X12 interchange object
}AcknolwedgeX12Options
interface AcknolwedgeX12Options {
input: X12Interchange; // X12 interchange object
}OperationResult
interface OperationResult {
LastIndex?: number;
Details?: Array<OperationDetail> | null;
Status?: string | null;
}OperationDetail
interface OperationDetail {
Index?: number;
TransactionIndex?: number;
TransactionRef?: string | null;
SpecRef?: string | null;
SegmentId?: string | null;
DataElementId?: string | null;
DataElementIndex?: number;
CompositeIndex?: number;
RepIndex?: number;
Value?: string | null;
Message?: string | null;
Status?: string | null;
}Logging and Tracing
The Temper EDI Client automatically creates spans for all operations and logs detailed information including:
- Operation type and timing
- Input and output data
- API request/response details
- Error details (if any)
- Validation results
- Acknowledgment status
Example Log Output
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"edi","edi":{"type":"x12","x12":{"input":"ISA*00*...","output":[...]}},"message":"X12 to JSON started."}
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"edi","edi":{"type":"x12","x12":{"input":"ISA*00*...","output":[...]}},"duration":1250,"message":"X12 to JSON completed."}Complete Example
import { TemperEdiClient } from '@ontemper/edi';
import { logger } from 'unnbound-logger-sdk';
const run = async () => {
const edi = new TemperEdiClient();
const x12Data = `ISA*00* *00* *12*NOTP *12*NOTP *080501*1700*U*00401*000001000*0*P*>
GS*PO*NOTP*NOTP*20080501*1700*1000*X*004010
ST*850*000000010
BEG*00*SA*08292243254**20010501*610385388
REF*DP*030
REF*PS*
ITD*14*3*2**45**46
DTM*001*20010510
PKG*F*68***PALLET, SHRINKWRAP 48W X 40D X 45H
PKG*F*66***REGULAR
TD5*A*92*P3**SEE ROUTING GUIDE FOR ROUTING
N1*ST*RETAIL STORE*9*0001234567890
N3*123 ANYWHERE AVENUE
N4*CITY*ST*12345
PO1*1*120*EA*9.25*TE*CB*(12) 0-083628-838*PR*RO*VN*ABA18783
PO1*2*220*EA*13.7 9*TE*CB*(69) 0-093 83 7-991*PR*RO*VN*RUP83112
PO1*3*126*EA*10.9 9*TE*CB*(71) 0-099172-837*PR*RO*VN*CPR19293
PO1*4*76*EA*4.35*TE*CB*(71) 0-012110-737*PR*RO*VN*PIW28173
PO1*5*72*EA*7.5*TE*CB*(71) 0-0848 88-9 75*PR*RO*VN*JBM1938 7
PO1*6*696*EA*9.55*TE*CB*(71) 0-003 922-121*PR*RO*VN*IUI19283
CTT*6
SE*20*000000010
GE*1*1000
IEA*1*000001000`;
try {
// Parse X12 to JSON
const x12Interchanges = await edi.fromX12({ input: x12Data });
logger.info({ count: x12Interchanges.length }, 'Parsed X12 interchanges');
// Process each interchange
for (const interchange of x12Interchanges) {
// Validate
const validation = await edi.validateX12({ input: interchange });
logger.info({ validation }, 'X12 validated');
// Convert back to X12
const x12Output = await edi.toX12({ input: interchange });
logger.info({ x12Output }, 'Converted back to X12');
// Generate and serialize acknowledgments. toX12 assigns control numbers
// after any partner-specific outbound envelope changes.
const acknowledgments = await edi.acknowledgeX12({ input: interchange });
for (const acknowledgment of acknowledgments) {
const acknowledgmentX12 = await edi.toX12({ input: acknowledgment });
logger.info({ acknowledgmentX12 }, 'Generated acknowledgment');
}
}
} catch (error) {
logger.error({ err: error }, 'EDI processing failed');
throw error;
}
};
run().catch(console.error);Control numbers
The SDK manages ISA13 and GS06 control numbers for each sender and receiver pair. toX12() automatically stamps
empty or all-zero ISA13, GS06, and ST02 fields. It preserves valid non-zero values. ISA13 and GS06 use the
centralized counter service. ST02 is assigned "0001".
Each Temper environment has a separate counter ledger. Production allocations continue the partner's production
sequence. Sandbox and staging allocations do not consume production control numbers. A ledger rolls over after
999999999, so the next value is 000000001.
Automatic environment routing
Workflow deployments inject TEMPER_ENVIRONMENT. The SDK reads TEMPER_ENVIRONMENT first and falls back to the
legacy UNNBOUND_ENVIRONMENT, which the platform still injects for older SDK versions. The value must be
sandbox, staging, or production. A missing or invalid value causes a ControlNumberError with the
control_number_environment_missing code before any HTTP request.
getNextControlNumber(), getCurrentControlNumber(), and toX12() use this environment. They do not accept an
environment override. ISA15 (UsageIndicator_15) does not select a counter, and toX12() does not change it.
import { getCurrentControlNumber, getNextControlNumber } from '@ontemper/edi';
const senderId = 'ZZ:SENDER';
const receiverId = 'ZZ:RECEIVER';
const next = await getNextControlNumber('ISA13', senderId, receiverId);
const current = await getCurrentControlNumber('ISA13', senderId, receiverId);The SDK has no seeding helper. Seed a counter through the Builder agent set_edi_control_number tool, which asks
the user to approve each seed. The tool value is the next number: seeding 4600 makes the next allocation 4600.
Seeding is forward-only. Re-seeding the same value is a no-op.
These helpers require UNNBOUND_API_URL. Temper configures it in workflow environments.
Requirements
- Node.js >= 22.0.0
- TypeScript (for TypeScript projects)
- The EDI connection env —
UNNBOUND_EDI_BASE_URL, plusUNNBOUND_EDI_API_KEYwhere the path needs a credential — is platform-injected per environment in Temper workflows; nothing to configure. Environments without an injected key pass the run's invocation token to the constructor instead; supply a key yourself only for the standalone legacy EDINation-cloud path. UNNBOUND_API_URL(for control number auto-stamping, pre-configured in workflow environments)TEMPER_ENVIRONMENTfor counter routing
Dependencies
unnbound-logger-sdk: Structured logging and tracing
Support
For support, please open an issue on the GitHub repository.
