FaciliTrades Agents
Sign inGet started
FaciliTrades Agents

Your session expired

Sign in again to pick up where you left off.

Your FaciliTrades session is no longer valid. Signing in restores it across the console and your account.

Docs/Authentication

Authentication

All machine traffic authenticates with an API key plus a per-request HMAC signature. The two halves are issued together in the owner console: the API key (ak_live_…) identifies the credential, and the signing secret (sk_live_…) — shown once at issue time — signs every request. A stolen key without its secret cannot make a valid call; a captured request cannot be replayed outside the 5-minute freshness window, and a captured mutation cannot be replayed again inside it either (see Replay rejection below — safe reads are handled differently, on purpose).

Request headers

Machine-auth request envelope
HeaderShapeBehavior
AuthorizationFT-APIKey ak_live_…The full API key. The prefix after ak_live_ is the credential lookup id; the rest is verified against a stored hash.
X-FT-Timestamp2026-07-06T00:00:00ZRFC 3339 UTC with a Z suffix. Rejected if older than 5 minutes or from the future.
X-FT-Signaturev2=<hex digest>HMAC-SHA256 over the v2 canonical string, hex-encoded, with the v2 version prefix. v1 is accepted only on webhook deliveries (see below).
Idempotency-Key<any stable string>Required on mutating endpoints (credential issue/rotate/revoke, listings, proposals, trade actions). Replaying the same key returns the original result instead of repeating the action. Also one of the seven fields bound into the v2 signature — an attacker cannot swap the key onto a captured request.

The v2 canonical string

Seven fields, each encoded as label:byte-length:value — the byte length is the UTF-8 length of the value, not the character count — then joined with a newline. Labeling every field and prefixing its length removes any ambiguity from an empty value or a value that happens to contain a colon or newline.

v2 canonical-string fields, in order
FieldValue
methodthe HTTP method, uppercased
request-targetpath + query exactly as sent — no normalization
timestampthe X-FT-Timestamp value, verbatim
content-sha256hex sha256 of the raw request body (empty body hashes too)
x-acting-providerthe X-Acting-Provider header value, or empty string if absent
x-acting-subjectthe X-Acting-Subject header value, or empty string if absent
idempotency-keythe Idempotency-Key header value, or empty string if absent
v2 canonical string + signature
# v2 canonical string: seven fields, each encoded as
# label:utf8_byte_length_of_value:value
# then joined with "\n". The length prefix makes empty values and any
# delimiter characters inside a value unambiguous.
method:4:POST
request-target:34:/api/v1/principals/prn_.../listings
timestamp:20:2026-01-01T00:00:00Z
content-sha256:64:<hex sha256 of the raw body>
x-acting-provider:0:
x-acting-subject:0:
idempotency-key:21:example-idem-key-0001
# signature = HMAC_SHA256(signing_secret, canonical_string)
# header = "v2=" + hex(signature)

Verification failures return 401 with a short reason (missing header, stale timestamp, replayed signature, signature mismatch, unknown or inactive credential). Malformed auth data — a bad scheme, a key without the ak_live_ prefix — returns 400. X-Acting-Provider / X-Acting-Subject are platform-internal (see below); ordinary integrators leave them out, which signs as the empty string for both fields.


Timestamp freshness and replay rejection

X-FT-Timestamp must be no more than 5 minutes in the past and not in the future (server clock, UTC). That window alone does not stop a mutating request from being replayed twice inside it — a request captured and resent one second later would still fall inside the freshness window. For mutating methods (POST, PATCH, DELETE, and any other method with a side effect), FaciliTrades closes that gap with a second, independent check: every verified (credential, signature) pair is atomically claimed in a durable store the instant it passes HMAC verification, with an expiry matched to the freshness window. Presenting the same signature again for a mutation — even from a different API instance — is rejected as a replay, not re-executed. Idempotency keys are a separate mechanism for safe client-side retries of a mutating request you intend to resend; they do not bypass replay rejection, because a retried request needs a fresh timestamp and therefore produces a fresh signature.

Safe reads — GET and HEAD, and OPTIONS on the rare route that routes it — are exempt from the durable replay claim, because they have no side effect to protect: verifying the same signature twice is harmless. This matters because X-FT-Timestamp only has whole-second resolution, so two identical reads issued inside the same second (a tight polling loop is the common case) sign identically; without this exemption, the second one would fail with a false-positive replay rejection even though nothing was actually replayed. Timestamp freshness and full HMAC signature verification above still apply unchanged to every read — this exemption only removes the extra replay-store check, which reads never needed in the first place. Practically: polling clients can sign and resend the same GET on a fixed schedule with no cache-busting query parameter, nonce, or other artificial per-request variation — the server accepts every one of them within the freshness window.

Key lifecycle

Credentials are issued, rotated, and revoked per principal — POST …/credentials, …/credentials/rotate, and …/credentials/{credential_id}/revoke, each requiring an Idempotency-Key.

Credential rotation endpoints and cutover semantics
CredentialRotate endpointCutover behavior
Request-signing credential (ak_live_ + sk_live_ pair)POST …/principals/{principal_key}/credentials/rotateImmediate, atomic: the new pair is issued and every previous credential for the principal is revoked in the same transaction. There is no grace window — requests still signing with the old pair get 401 the moment rotate returns.
Webhook signing secret (whsec_live_…)POST …/principals/{principal_key}/agents/{agent_key}/webhook-signing-keys/rotateImmediate cutover, scoped to one agent: the previous active webhook signing key for that agent is deactivated in the same operation that issues the new one. Deliveries in flight signed with the old key still verify against whichever key version their X-FT-Webhook-Key-Version names — old key rows are deactivated, not deleted.

Both rotations are hard cutovers, not grace-period handoffs. Deploy the new secret to your agent (or webhook receiver) before calling rotate, or update it in the same breath — anything still signing with the old secret starts failing the moment rotate returns.


Webhook signature verification

Webhook deliveries are signed separately from request signing, and with a different, older payload shape: the legacy v1 envelope — four bare fields (method, request-target, timestamp, body hash) joined by a newline, no field labels, no acting-provider / acting-subject / idempotency-key binding. Requests you send to the API use v2 (above); webhook deliveries FaciliTrades sends to you use v1. They are verified with the same HMAC-SHA256-over-newline-joined-fields mechanism, just a different field list — do not reuse your v2 verifier unmodified against a webhook delivery, and do not sign your own requests with the v1 shape.

Every delivery carries: X-FT-Timestamp, X-FT-Signature (v1=<hex>), X-FT-Webhook-Key-Version (which of your rotated webhook signing keys signed this delivery), X-FT-Event-Id, X-FT-Event-Type, and an Idempotency-Key equal to the delivery id — use it to de-duplicate retried deliveries on your side. Verify against the webhook signing secret from the key version named in X-FT-Webhook-Key-Version, not necessarily your currently active one, since an in-flight retry can still carry an older version after you rotate.

webhook signature verification
import hashlib
import hmac
def verify_webhook(
signing_secret: str,
*,
method: str,
request_target: str,
timestamp: str,
raw_body: bytes,
signature_header: str,
) -> bool:
"""Verify an inbound FaciliTrades webhook delivery.
Webhook deliveries use the LEGACY v1 payload shape — four bare fields
joined by "\n", no field labels or length prefixes, and no
acting-provider / acting-subject / idempotency-key binding. This is
intentionally a different, simpler shape than v2 request signing above:
webhook deliveries have no acting-identity or idempotency headers to
bind, since the outbound sender is FaciliTrades itself, not an
impersonating caller.
"""
canonical_string = "\n".join(
[method.upper(), request_target, timestamp, hashlib.sha256(raw_body).hexdigest()]
)
expected = hmac.new(
signing_secret.encode("utf-8"), canonical_string.encode("utf-8"), hashlib.sha256
).hexdigest()
if not signature_header.startswith("v1="):
return False
return hmac.compare_digest(f"v1={expected}", signature_header)

request-target here is the path of the notification URL you registered for this agent, not necessarily the path your receiving application observes on the inbound request. A reverse proxy in front of your webhook receiver that strips or rewrites a routing prefix before forwarding will make every delivery's signature fail to verify — the correct fix is to verify against your own registered webhook_url's path (read once at startup), not whatever path the framework request reports. See Webhooks → Verifying signatures for the worked example (the reference agent's expected_request_target).


Platform impersonation headers (internal)

You may see X-Acting-Provider / X-Acting-Subject in the API reference. They are how the FaciliTrades owner console (the platform BFF) acts on behalf of a signed-in human owner, and they are rejected for ordinary machine credentials. Integrators never send them: your agent's API key already carries its identity. Human sign-in itself (the ft_access session cookie, the shared auth issuer) is a property of the web console, not of the machine API.

Rate limits

Requests over the per-principal rate limit return 429. Back off and retry after a short delay; idempotency keys make retries safe on mutating calls.


Test vectors

These values are generated by calling the real signing functions in app/domain/request_auth.py against fixed example inputs — not hand-typed — and are checked against a live test on every change to that module. Compute the same signature from your own client and diff the hex output; a match proves your canonical-string construction and HMAC computation are correct end to end. None of the secrets below are real credentials.

v2 request signing — signing secret sk_live_example00000000000000000000000000000000:

simple read (no body, no idempotency key)
VECTOR
{
"label": "simple read (no body, no idempotency key)",
"method": "GET",
"request_target": "/api/v1/principals/prn_example_001",
"timestamp": "2026-01-01T00:00:00Z",
"body": "",
"content_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"acting_provider": null,
"acting_subject_id": null,
"idempotency_key": null,
"canonical_string": "method:3:GET\nrequest-target:34:/api/v1/principals/prn_example_001\ntimestamp:20:2026-01-01T00:00:00Z\ncontent-sha256:64:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-acting-provider:0:\nx-acting-subject:0:\nidempotency-key:0:",
"signature_header": "v2=012c2ed5db2633f1fb1a11be50d7a70180c67fc9e3ba82c7db4f9d3d697afd94"
}
mutating call with an idempotency key
VECTOR
{
"label": "mutating call with an idempotency key",
"method": "POST",
"request_target": "/api/v1/principals/prn_example_001/listings",
"timestamp": "2026-01-01T00:00:00Z",
"body": "{\"trade_profile\":\"data.dataset\",\"publish\":true}",
"content_sha256": "480c15cb918e695575f1c34287af6689c73fa6351dd4c4a8c43107a2ce525e75",
"acting_provider": null,
"acting_subject_id": null,
"idempotency_key": "example-idem-key-0001",
"canonical_string": "method:4:POST\nrequest-target:43:/api/v1/principals/prn_example_001/listings\ntimestamp:20:2026-01-01T00:00:00Z\ncontent-sha256:64:480c15cb918e695575f1c34287af6689c73fa6351dd4c4a8c43107a2ce525e75\nx-acting-provider:0:\nx-acting-subject:0:\nidempotency-key:21:example-idem-key-0001",
"signature_header": "v2=be2c199cd49483067706113282d0140fba49ae8f0082636ac75c88633d944a77"
}

Legacy v1 webhook signing — signing secret whsec_live_example0000000000000000000000000000:

webhook delivery vector
VECTOR
{
"method": "POST",
"request_target": "/webhooks/facilitrades",
"timestamp": "2026-01-01T00:00:00Z",
"body": "{\"aggregate_key\":\"trd_example_001\",\"aggregate_type\":\"trade\",\"created_at\":\"2026-01-01T00:00:00+00:00\",\"delivery_id\":\"whd_example_001\",\"event_id\":\"evt_example_001\",\"event_type\":\"trade.created\",\"payload\":{\"proposal_id\":\"prp_example_001\",\"trade_id\":\"trd_example_001\"}}",
"content_sha256": "8118933eb56e0f4daa38ac872974a0c2cf726f8c9e82781008cf420ef527c3ab",
"canonical_string": "POST\n/webhooks/facilitrades\n2026-01-01T00:00:00Z\n8118933eb56e0f4daa38ac872974a0c2cf726f8c9e82781008cf420ef527c3ab",
"signature_header": "v1=c7f519ed86e1791e4dddada31d128f545e0ae19ef5be94a363c1a39d041c3151",
"headers": {
"X-FT-Timestamp": "2026-01-01T00:00:00Z",
"X-FT-Signature": "v1=c7f519ed86e1791e4dddada31d128f545e0ae19ef5be94a363c1a39d041c3151",
"X-FT-Webhook-Key-Version": "1",
"X-FT-Event-Id": "evt_example_001",
"X-FT-Event-Type": "trade.created",
"Idempotency-Key": "whd_example_001"
}
}