Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions code/API_definitions/click-to-dial.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,13 @@ paths:
parameters:
- $ref: "../common/CAMARA_common.yaml#/components/parameters/x-correlator"
security:
- {}
- notificationsBearerAuth: []
description: |
Event notification delivered to the sink URL when the call status
changes (CloudEvents structured mode).
The sink uses HTTPS and may accept notifications without Bearer authentication when no `sinkCredential` was provided.
If `sinkCredential` was provided, the provider uses the specified authentication mechanism.
requestBody:
required: true
content:
Expand All @@ -134,7 +137,7 @@ paths:
$ref: '#/components/examples/CALL_STATUS_CHANGED_EXAMPLE'
responses:
'2XX':
description: Event successfully received by the sink.
description: Event successfully received by the sink, including 204 No Content.
headers:
x-correlator:
$ref: "../common/CAMARA_common.yaml#/components/headers/x-correlator"
Expand All @@ -144,6 +147,10 @@ paths:
$ref: '../common/CAMARA_common.yaml#/components/responses/Unauthenticated401'
'403':
$ref: '../common/CAMARA_common.yaml#/components/responses/PermissionDenied403'
'410':
$ref: '../common/CAMARA_event_common.yaml#/components/responses/SinkGone410'
'429':
$ref: '../common/CAMARA_common.yaml#/components/responses/TooManyRequests429'
/calls/{callId}:
get:
tags:
Expand Down Expand Up @@ -294,6 +301,13 @@ components:
$ref: '#/components/schemas/CallStatus'
createdAt:
$ref: '../common/CAMARA_common.yaml#/components/schemas/DateTime'
sink:
description: |
The HTTPS notification endpoint registered for this call, if any.
When provided and accepted during call creation, the same sink MUST be returned in the 201 response and subsequent GET responses.
Notification credentials and other secrets are not returned in the Call resource.
allOf:
- $ref: '../common/CAMARA_event_common.yaml#/components/schemas/Sink'
recordingEnabled:
type: boolean
description: Whether recording is enabled for this call.
Expand All @@ -308,7 +322,10 @@ components:
description: |
Request to create a new Click to Dial call session.
If a `sink` URL is provided, status notifications (CloudEvents in structured mode) will be delivered to it.
Without `sink`, no implicit event subscription is created. `sinkCredential` is optional; without it, the HTTPS sink does not require Bearer authentication.
If a `sinkCredential` of type `ACCESSTOKEN` is provided, the access token is used as a Bearer token in the `Authorization` header during event delivery.
For `ACCESSTOKEN`, `sinkCredential.accessTokenExpiresUtc` is the absolute UTC timestamp at which the access token shall be considered expired. Token expiration SHOULD occur after the end of the call's lifetime, allowing the consumer to receive notifications throughout the call.
If the token expires while the call is still active, the consumer will stop receiving notifications.
Callback delivery failure does not roll back or cancel a successfully created call session; API consumers can query `GET /calls/{callId}` to reconcile state.
type: object
additionalProperties: false
Expand Down Expand Up @@ -336,7 +353,7 @@ components:
sinkCredential:
credentialType: ACCESSTOKEN
accessToken: "ya29.A0AR...exampletoken"
accessTokenExpiresUtc: "2026-01-01T00:00:00Z"
accessTokenExpiresUtc: "2027-12-31T23:59:59Z"
accessTokenType: bearer
recordingEnabled: true

Expand Down
30 changes: 29 additions & 1 deletion code/Test_definitions/click-to-dial-createCall.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Feature: CAMARA Click to Dial API, vwip - Operation createCall
#
# Implementation indications:
# * apiRoot: API root of the server URL
# * A notification-capable provider and monitored HTTPS sinks with and without Bearer authentication are needed for event scenarios.
# * These are declarative test definitions; subscription inspection and new absence assertions require implementation-specific step bindings.
#
# References to OAS spec schemas refer to schemas specified in click-to-dial.yaml

Expand All @@ -22,6 +24,8 @@ Feature: CAMARA Click to Dial API, vwip - Operation createCall
Scenario: Common validations for createCall success scenario
Given the request property "$.caller" is set to a valid caller number in E.164 format
And the request property "$.callee" is set to a valid callee number in E.164 format
And the request property "$.sink" is removed from the request body
And the request property "$.sinkCredential" is removed from the request body
When the request "createCall" is sent
Then the response status code is 201
And the response header "Content-Type" is "application/json"
Expand All @@ -30,23 +34,47 @@ Feature: CAMARA Click to Dial API, vwip - Operation createCall
And the response property "$.callId" exists
And the response property "$.caller" has the same value as createCall request property "$.caller"
And the response property "$.callee" has the same value as createCall request property "$.callee"
And the response property "$.sink" does not exist
And no implicit event subscription is created for the call

@createcall_event_notification
Scenario: Events are received when sink and access token credential are provided
Given the request property "$.caller" is set to a valid caller number in E.164 format
And the request property "$.callee" is set to a valid callee number in E.164 format
And the request property "$.sink" is set to a URL where events can be monitored
And the request property "$.sink" is set to a valid HTTPS URL where events can be monitored
And the request property "$.sinkCredential.credentialType" is set to "ACCESSTOKEN"
And the request property "$.sinkCredential.accessTokenType" is set to "bearer"
And the request property "$.sinkCredential.accessToken" is set to a valid access token accepted by the events receiver
And the request property "$.sinkCredential.accessTokenExpiresUtc" is set to a future RFC3339 timestamp
When the request "createCall" is sent
Then the response status code is 201
And the response body complies with the OAS schema at "/components/schemas/Call"
And the response property "$.sink" has the same value as createCall request property "$.sink"
And the response property "$.sinkCredential" does not exist
And the response property "$.accessToken" does not exist
And the response body does not contain the sink credential access token
And an event is received at the address of the request property "$.sink"
And the event header "Authorization" is set to "Bearer " + the value of the request property "$.sinkCredential.accessToken"
And the event header "Content-Type" is "application/cloudevents+json"
And the event body complies with the OAS schema at "#/components/schemas/CallStatusChangedEvent"

@createcall_event_notification_without_credential
Scenario: Events are received when sink is provided without sinkCredential
Given the request property "$.caller" is set to a valid caller number in E.164 format
And the request property "$.callee" is set to a valid callee number in E.164 format
And the request property "$.sink" is set to a valid HTTPS URL where events can be monitored without Bearer authentication
And the request property "$.sinkCredential" is removed from the request body
When the request "createCall" is sent
Then the response status code is 201
And the response body complies with the OAS schema at "/components/schemas/Call"
And the response property "$.sink" has the same value as createCall request property "$.sink"
And the response property "$.sinkCredential" does not exist
And the response property "$.accessToken" does not exist
And an event is received at the address of the request property "$.sink"
And the event header "Authorization" does not exist
And the event header "Content-Type" is "application/cloudevents+json"
And the event body complies with the OAS schema at "#/components/schemas/CallStatusChangedEvent"

@createcall_event_notification
Scenario: Event body fields follow ClickToDial status rules
Given the request property "$.caller" is set to a valid caller number in E.164 format
Expand Down
17 changes: 17 additions & 0 deletions code/Test_definitions/click-to-dial-getCall.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Feature: CAMARA Click to Dial API, vwip - Operation getCall
#
# Implementation indications:
# * apiRoot: API root of the server URL
# * The registered-sink scenario needs a call created with a monitored HTTPS sink and an accepted ACCESSTOKEN sinkCredential.
# * These are declarative test definitions; the registered-sink setup and new absence assertions require implementation-specific step bindings.
#
# References to OAS spec schemas refer to schemas specified in click-to-dial.yaml

Expand All @@ -26,6 +28,21 @@ Feature: CAMARA Click to Dial API, vwip - Operation getCall
And the response header "x-correlator" has the same value as the request header "x-correlator"
And the response body complies with the OAS schema at "/components/schemas/Call"

@getcall_success_registered_sink
Scenario: Get call details preserves the registered sink without exposing credentials
Given a valid call identifier obtained from a previous createCall response
And that call was created with a registered HTTPS sink and an accepted ACCESSTOKEN sinkCredential
And the request path parameter "callId" is set to that identifier
When the request "getCall" is sent
Then the response status code is 200
And the response header "Content-Type" is "application/json"
And the response header "x-correlator" has the same value as the request header "x-correlator"
And the response body complies with the OAS schema at "/components/schemas/Call"
And the response property "$.sink" has the same value as createCall request property "$.sink"
And the response property "$.sinkCredential" does not exist
And the response property "$.accessToken" does not exist
And the response body does not contain the sink credential access token

@getcall_failure_malformed_callid
Scenario: Fail to get call details due to malformed callId path parameter
Given the request path parameter "callId" is set to a value that does not comply with the CallId schema
Expand Down
14 changes: 10 additions & 4 deletions documentation/API_documentation/click-to-dial_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ curl -X POST "{apiRoot}/calls" \
"sinkCredential": {
"credentialType": "ACCESSTOKEN",
"accessToken": "{sink_access_token}",
"accessTokenExpiresUtc": "2025-12-31T23:59:59Z",
"accessTokenExpiresUtc": "2027-12-31T23:59:59Z",
"accessTokenType": "bearer"
},
"recordingEnabled": true
Expand All @@ -49,6 +49,7 @@ curl -X POST "{apiRoot}/calls" \
"callee": { "number": "+447700900123" },
"status": "initiating",
"createdAt": "2025-12-11T12:00:00Z",
"sink": "https://yourapp.example.com/clicktodialstatusnotify",
"recordingEnabled": true
}
```
Expand Down Expand Up @@ -153,7 +154,7 @@ The SinkCredential is a discriminator-based object defined by CAMARA Commonaliti
| --------------------- | --------------------------------------------- | -------- | ---------------------- |
| credentialType | Must be `ACCESSTOKEN` (discriminator) | Yes | `ACCESSTOKEN` |
| accessToken | Access token used to authenticate event POSTs | Yes | `sink_token` |
| accessTokenExpiresUtc | UTC expiry timestamp for the access token | Yes | `2025-12-31T23:59:59Z` |
| accessTokenExpiresUtc | UTC expiry timestamp for the access token | Yes | `2027-12-31T23:59:59Z` |
| accessTokenType | Token type (OAuth token type) | Yes | `bearer` |

##### Click to Dial Status Notify (callback payload)
Expand Down Expand Up @@ -250,11 +251,16 @@ Example CloudEvent (structured mode) — `CALL_STATUS_CHANGED_EXAMPLE` from the

### Callback Authentication, Retries, and Resilience

* **Authentication**: When delivering events to a client's `sink` URL that requires authentication, the provider MUST use the provided `sinkCredential`. If the credential type is `ACCESSTOKEN`, the provider MUST set the HTTP `Authorization` header to `Authorization: Bearer <accessToken>` (using the value of the `accessToken` field from the request).
* **Optional callback authentication**: `sinkCredential` is optional in the CAMARA implicit subscription model. Without it, notifications can be delivered to an HTTPS `sink` that does not require Bearer authentication. When a credential is provided, the provider MUST use the specified authentication mechanism; for `ACCESSTOKEN`, it MUST set `Authorization: Bearer <accessToken>` using the supplied token. Authenticated callbacks are recommended for production. The callback runs from Provider to Consumer; creating a call still requires the Consumer's OAuth/OpenID access token for the Provider API.
* **Token lifetime (`ACCESSTOKEN`)**: `accessTokenExpiresUtc` is the absolute UTC timestamp at which the access token shall be considered expired. Token expiration SHOULD occur after the end of the call's lifetime, allowing the consumer to receive notifications throughout the call. If the token expires while the call is still active, the consumer will stop receiving notifications, as specified in [Commonalities r4.4 Event Guide §3.4](https://github.com/camaraproject/Commonalities/blob/r4.4/documentation/CAMARA-API-Event-Subscription-and-Notification-Guide.md#34-subscription-ended-event).
* **Subscription and sink echo**: Without `sink`, no implicit notification subscription is created. If a supplied `sink` is accepted, the 201 response MUST echo the same URL, and subsequent `GET /calls/{callId}` responses return the registered sink. Call responses do not return `sinkCredential`, access tokens, or other credential secrets. If notification delivery is unsupported, the existing `422 EVENT_NOTIFICATIONS_NOT_SUPPORTED` response applies and no call is created.
* **Consumer verification**: Callbacks without authentication risk forged events and spoofed call state. Consumers SHOULD validate the CloudEvent `id`, `time`, `source`, `type`, and event schema, and handle duplicates and replay according to their policy. Bearer authentication alone does not prevent all replay attacks. Do not trigger sensitive or irreversible actions solely from an unverified callback; use `GET /calls/{callId}` to confirm authoritative state when needed.
* **Delivery Retries**: Providers SHOULD ensure event `id` uniqueness. Transient callback delivery failures (e.g., connection timeouts, DNS resolution failures, or receiving `5xx` server error responses from the client's `sink` URL) may be retried by the provider according to its internal retry and backoff policy.
* **Session Resilience**: A callback delivery failure does **not** roll back or cancel a successfully created call session. The call session will continue to be processed and connected normally.
* **State Reconciliation**: API consumers can actively reconcile the call session state at any time by calling `GET /calls/{callId}` to retrieve the current status of the call. This is particularly useful if status notification events are delayed or fail to be delivered.

The shared r4.4 `AccessTokenCredential.accessTokenExpiresUtc` schema description still refers to explicit-subscription `ACCESS_TOKEN_EXPIRED` termination notifications. CTD uses the call-specific implicit-subscription guidance above; the shared credential schema is reused unchanged.

## Call flow (sequence diagram)

The following sequence diagram shows the typical flow when creating a Click-to-Dial session, delivering events, and terminating the call. This is a logical flow; implementers may optimize or parallelise steps.
Expand Down Expand Up @@ -297,7 +303,7 @@ curl -X POST "{apiRoot}/calls" \
"sinkCredential": {
"credentialType": "ACCESSTOKEN",
"accessToken": "{sink_access_token}",
"accessTokenExpiresUtc": "2025-12-31T23:59:59Z",
"accessTokenExpiresUtc": "2027-12-31T23:59:59Z",
"accessTokenType": "bearer"
}
}'
Expand Down
Loading