# Transports

Pick one or more. Each transport is its own connection with its own `source_id`.

## 6.1 Webhook

```
POST https://<api host>/promo/v1/ingest/<source_id>
Content-Type: application/json
X-Promo-Timestamp: <unix seconds>
X-Promo-Signature: <signature>
```

```
signature = lowercase_hex( HMAC-SHA256( secret, X-Promo-Timestamp + "." + raw_body ) )
```

- Sign **the exact bytes you send**. Serialising the body again after signing (another key order,
  spaces) breaks the signature.
- `X-Promo-Timestamp` must be within **±300 s** of our clock.
- **Rotation.** During a rotation we accept both the current and the next secret: we give you the
  next one, you switch to it, we retire the old one.
- **Body.** One event, or `{"events": [ … ]}` with up to **500** events; at most **1 MiB**. Other
  limits can be agreed in the form.
- **Concurrency.** The number of requests in flight is agreed in the form; a request that finds no
  free slot within 0.5 s is answered `503`.

| Status | Body | Meaning | Resend? |
| :--- | :--- | :--- | :--- |
| `200` | `{"source":"<source_id>","results":[{"index":0,"event_id":"…","verdict":"accepted"}, …]}` | every event is decided; `verdict` is `accepted`, `skipped`, `rejected` or `duplicate`; `skipped` and `rejected` carry a `reason` (Appendix A). A `duplicate` may carry one too — ignore it, the event was decided earlier | no |
| `503` + `Retry-After: 1` | `{"source":"<source_id>","error":"undecided","results":[ …decided so far… ]}` | we could not decide an event right now; events after it were not tried | **yes — the same batch, the same `event_id`s** |
| `401` | `{"error":"unauthorized","results":[]}` | missing or stale timestamp, missing signature, or a signature that matches no secret | after fixing the request |
| `413` | `{"error":"body_too_large","results":[]}` / `{"error":"batch_too_large","results":[]}` | over 1 MiB, or more events than the batch limit | after splitting |
| `400` | `{"error":"malformed","results":[]}` | signed, but not a JSON object, or `events` is not an array | after fixing |
| `404` | `{"error":"unknown_source","results":[]}` | no active connection with this `source_id` | contact us |

On `503`, any other `5xx` or a network error, resend the same batch: events already decided answer
`duplicate`, the rest are decided. `{"events": []}` answers `200` with empty `results`. If a
connection keeps answering `503`, contact us.

Examples (standard library only), each sends one file from `examples/events/`:

```bash
export PROMO_INGEST_URL=https://api.rexplay.site/promo/v1/ingest/<source_id>
read -rs PROMO_SOURCE_SECRET && export PROMO_SOURCE_SECRET
cd examples/webhook
go run . ../events/bet.json
python3 send.py ../events/settled.json
```

```
200  {"source":"<source_id>","results":[{"index":0,"event_id":"01J8Z3K9T6-bet-778812","verdict":"accepted"}]}
```

## 6.2 Kafka (your cluster)

We connect to your cluster as a consumer.

- **You give us** (form, and credentials once — §10.2): the bootstrap brokers, **every broker
  address the cluster advertises** with its IP, the topics, TLS — the CA, and the client certificate
  with its key for mutual TLS, plus the TLS server name — and SASL (`PLAIN`, `SCRAM-SHA-256` or
  `SCRAM-SHA-512`) with a user that may read the topics and use our consumer group. We send you the
  group name for your ACLs.
- **TLS is required** in production.
- **Message.** The record value is one event (§5.2), JSON in UTF-8. The key and the headers are not
  read: key the topic as your own ordering needs.
- **Start.** We read from the moment we connect; tell us in the form if we should also read what the
  topic already retains.
- **Delivery.** We commit an offset only after its event is decided. An event we could not decide is
  read again, so delivery is at least once; `event_id` absorbs the repeats.

## 6.3 RabbitMQ (your broker)

AMQP 0-9-1 over TLS (`amqps://`, required in production). You give us the host, port, vhost, user and
password, and one of:

- **an exchange and routing keys** — we declare our own durable queue and bind it to your exchange.
  You declare the exchange; our user needs the right to declare and bind that queue. The queue holds
  only what arrives after we create it; or
- **a queue you created for us** — our user needs the right to consume it.

- **Message.** The body is one event (§5.2). Headers and properties are not read. Publish
  persistently so an event survives a broker restart.
- **Delivery.** We acknowledge a message only after its event is decided. One we could not decide
  stays unacknowledged and we retry it after 1 s, doubling up to 30 s; we never requeue it. If our
  connection drops, the broker redelivers what we had not acknowledged, and a decided event answers
  `duplicate`. Several of our consumers may read one queue; order is not required.
