ZuloOne
English
Sign in
Menu

Integrate with a third-party REST API

Updated

A shop, a WMS, a bank, or a tax operator talks HTTP. Your script names a channel or publishes a contract. It does not open a socket.

Note

Never HttpClient / System.Net.Http from a script. Name the channel. The host owns URL, secret, signature, and SSRF. What a script can do.

Pick one door#

They need Use
we must not lose a posted document IOutboundGateway
response is single-use now IOutboundCall
they CRUD our records Data entity
they trigger one operation Web service

Do not mix these in one paragraph of design. A posted shipment that must reach WMS is a queue. An OTP swapped for a CSID is a call. Items they sync are an OData collection. One operation they invoke is a web service.

1. We call them (outbound)#

Channel metadata: OutboundChannels/<name>.channel.json in your model, kind OutboundChannel. Path, method, signer, authMode. The tenant's live URL, timeout, and secret live on the connection dictionary named in connectionTable — never in git.

Then pick the send door. The choice is not style.

IOutboundGateway IOutboundCall
What a queue (outbox) a synchronous call
When a document must be sent an exchange needs its response now
Retry yes no

Document submit — IOutboundGateway.EnqueueAsync. The outbox row is written in the same transaction as posting, so a rolled-back post sends nothing. The host retries. Pass the channel name, not a URL.

Need the response now (OTP → CSID) — IOutboundCall.SendAsync. A queue row would log the secret; a retry would present a spent OTP.

// Example only — channel `wms-ship` is invented. Inventory.GoodsIssue
// has no WMS enqueue today.
await ScriptServices.Get<IOutboundGateway>().EnqueueAsync(new OutboundRequest
{
    Channel = "wms-ship",
    IdempotencyKey = "wms:" + document.MetaId.ToString("N"),
    Payload = body,
    SourceTable = "GoodsIssue",
    SourceRecordId = document.MetaId,
});

var result = await ScriptServices.Get<IOutboundCall>().SendAsync(
    "wms-auth", payload, idempotencyKey: "wms-otp");

Enqueue from the posting path (a service the document calls as it posts), not from a job after the fact. OnAfterPostAsync can run twice — the IdempotencyKey is the guard.

Bump modelVersion in the same change. A new channel also needs POST /api/services/rebuild.

Warning

A successful send is not a receipt. Ask whether the response is the other side's decision, or "accepted for processing". Closing the document on Succeeded when a receipt still has to be fetched registers work the other system has not seen.

InDoubt means the request left and no answer came back. The document may already be registered. Do not retry that status. Query fate; do not submit again.

There is no inbound callback. Drain your own outbox for send outcomes; poll the other side with IOutboundCall when you need a receipt. A web service you publish is not that callback: they call an operation we named; they do not complete our send.

Signing, four guards against a second submit, TakeCompletedAsync: Outbound integrations. Short send recipe: Send an outbound call.

2. They pull/push records (OData Data Entity)#

Collection at /odata/{Collection}. Use this when the other system wants a table (items, orders), not an RPC. Mapping, events, rights: Create a data entity, Data Entity.

3. They call an RPC (web service)#

/api/rest/{Name}. Do not add a platform controller. Outbound from that service still goes by channel name. Recipe: Create a web service. Contract: A model's web services.

The other direction — they already have a token and need our surface — is Call ZuloOne from another system.

What's not there yet#

The platform does not accept an inbound callback from outside. CompletionHandler is reserved and unread. An integration test cannot cover the HTTP exchange itself: cover everything before the send and everything after the response; say so in the test header.

A web service at /api/rest/{Name} is inbound RPC we publish. That is not a webhook they fire at us unsolicited, and it does not complete an outbound outbox row.

Where next#

Was this page helpful?