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.Httpfrom 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
Succeededwhen a receipt still has to be fetched registers work the other system has not seen.
InDoubtmeans 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
IOutboundCallwhen 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.