Get consent events

Consent events are events triggered by granting, updating or revoking consents for your product, either for a single user or for an operator. The events can originate from

  • Bundling of your product for a subscriber
  • Preview consent added by Working Group Two on your behalf
  • Operator consent covering all subscribers for an operator
  • Subscriber signing up to your product

Prerequisites

  1. An OAuth 2.0 client
  2. A client access token

Required scope

None

Code

TIP

You can test our APIs without authorization by targetting sandbox.api.wgtwo.com instead of api.wgtwo.com and removing any authorization from the request/code sample.

Download proto definitions
curl -sL 'https://github.com/working-group-two/wgtwoapis/blob/master/image.bin?raw=true' -o wgtwo.bin
1
export ACCESS_TOKEN="my_client_access_token"
grpcurl -protoset wgtwo.bin \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '
  {
    "stream_configuration": {
      "regular": {},
      "disable_explicit_ack": {}
    }
  }
  ' \
  api.wgtwo.com:443 \
  wgtwo.consents.v1.ConsentEventService/StreamConsentChangeEvents
1
2
3
4
5
6
7
8
9
10
11
12
13
About stream_configuration

For testing purposes, we include the config:

"stream_configuration": {
  "regular": {},              Reading position will not be stored in the server and load is not spread between your clients 
  "disable_explicit_ack": {}  Let events be automatically acked
}
1
2
3
4

By default, load will automatically be spread between all connections using the same OAuth 2.0 client and you will need to reply with a ack once your service has handled the message. This is also what we would recommend for real production usage.

See configuring event streaming for details.

Example result

{
"metadata": {
  "timestamp": "2022-08-09T12:22:57.110Z",
  "identifier": {
    "subscriptionIdentifier": {
      "value": "490e6f4a2cccd6067741b8a2a4bb14b2a1b2756c523c6c818e7eaee651b07743e69383304f45b3f6970b8bd01d16f845bc999d1ff036a5e605fec314d8aed99e"
    }
  },
  "ackInfo": {
    "value": "$JS.ACK.consent_stream.H5yg2gnj.1.9.1.1660047797010497111.0"
  }
},
"consentChangeEvent": {
  "added": {
    "scopes": [ "sms.text:send_to_subscriber" ]
  },
  "number": {
    "e164": "+46724452050"
  }
}
}

{
"metadata": {
  "timestamp": "2022-08-09T12:23:17.010Z",
  "identifier": {
    "subscriptionIdentifier": {
      "value": "490e6f4a2cccd6067741b8a2a4bb14b2a1b2756c523c6c818e7eaee651b07743e69383304f45b3f6970b8bd01d16f845bc999d1ff036a5e605fec314d8aed99e"
    }
  },
  "ackInfo": {
    "value": "$JS.ACK.consent_stream.H5yg2gnj.1.9.1.1660047797010497633.0"
  }
},
"consentChangeEvent": {
  "revoked": {

  },
  "number": {
    "e164": "+46724452050"
  }
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Install dependencies

Maven

<dependency>
  <groupId>com.wgtwo.api.v1.grpc</groupId>
  <artifactId>consent-events</artifactId>
  <version>1.8.0</version>
</dependency>

search.maven.org/search?q=g:com.wgtwo.api.v1.grpcopen in new window

package com.example.consents

import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.wgtwo.api.v1.consent.ConsentEventServiceGrpc
import com.wgtwo.api.v1.consent.ConsentEventsProto
import com.wgtwo.api.v1.consent.ConsentEventsProto.AckConsentChangeEventRequest
import com.wgtwo.api.v1.consent.ConsentEventsProto.AckConsentChangeEventResponse
import com.wgtwo.api.v1.consent.ConsentEventsProto.StreamConsentChangeEventsResponse
import com.wgtwo.api.v1.events.EventsProto
import com.wgtwo.api.v1.events.EventsProto.AckInfo
import io.grpc.CallCredentials
import io.grpc.Context
import io.grpc.ManagedChannelBuilder
import io.grpc.Metadata
import io.grpc.Status
import io.grpc.StatusException
import io.grpc.StatusRuntimeException
import io.grpc.stub.StreamObserver
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.Supplier

fun main() {
    EventHandler.start()
    Thread.sleep(60_000)
    EventHandler.close()
}

object EventHandler {
    private val isSandboxed = true // Toggle this to go live
    private val api = if (!isSandboxed) "api.wgtwo.com" else "sandbox.api.wgtwo.com"
    private val channel = ManagedChannelBuilder.forAddress(api, 443).build()
    private val consentEvents = ConsentEventServiceGrpc.newStub(channel).apply {
        if (!isSandboxed) { // credentials are needed outside sandbox environment
            this.withCallCredentials(BearerToken { "MY_CLIENT_ACCESS_TOKEN" })
        }
    }

    private val maxInFlight: Int = EventsProto.StreamConfiguration.MAX_IN_FLIGHT_FIELD_NUMBER
    private val context = Context.current().fork().withCancellation()
    private val started = AtomicBoolean()
    private val streamExecutor = Executors.newSingleThreadScheduledExecutor()
    private val processExecutor = Executors.newFixedThreadPool(maxInFlight, thread("event-pool-%d"))

    fun start() {
        if (started.getAndSet(true)) {
            return
        }
        streamExecutor.submit(::run)
    }

    private fun run() {
        println("Starting subscription")
        val request =
            ConsentEventsProto.StreamConsentChangeEventsRequest.newBuilder()
                .setStreamConfiguration(
                    EventsProto.StreamConfiguration.newBuilder()
                        .setMaxInFlight(maxInFlight)
                )
                .build()
        context.run {
            consentEvents.streamConsentChangeEvents(
                request,
                consentObserver
            )
        }
    }

    private fun ack(ackInfo: AckInfo) {
        val request = AckConsentChangeEventRequest.newBuilder().setAckInfo(ackInfo).build()
        consentEvents.ackConsentChangeEvent(request, ackObserver)
    }

    private val consentObserver = object : StreamObserver<StreamConsentChangeEventsResponse> {
        override fun onNext(response: StreamConsentChangeEventsResponse) {
            processExecutor.submit {
                try {
                    println(response)
                } catch (e: Exception) {
                    println("Could not process event. e = $e")
                } finally {
                    ack(response.metadata.ackInfo)
                }
            }
        }

        override fun onError(t: Throwable) {
            val code = code(t)
            if (code == Status.Code.CANCELLED) {
                println("Cancelled - Good bye!")
                return
            }
            val delaySeconds =
                when (code) {
                    Status.Code.UNAUTHENTICATED, Status.Code.PERMISSION_DENIED -> 10L
                    else -> 1L
                }
            println("Got onError with code=${code} - restart in $delaySeconds seconds")
            streamExecutor.schedule(::run, delaySeconds, TimeUnit.SECONDS)
        }

        override fun onCompleted() = Unit

        fun code(throwable: Throwable): Status.Code =
            when (throwable) {
                is StatusRuntimeException -> throwable.status.code
                is StatusException -> throwable.status.code
                else -> Status.Code.UNKNOWN
            }
    }

    private val ackObserver = object : StreamObserver<AckConsentChangeEventResponse> {
        override fun onNext(value: AckConsentChangeEventResponse) =
            when (value.ackStatus.statusCode) {
                EventsProto.AckStatus.StatusCode.STATUS_CODE_SUCCESS ->
                    println("Ack success")
                else -> println("Ack failed: ${value.ackStatus}")
            }

        override fun onError(t: Throwable) = println("onError. $t")
        override fun onCompleted() = Unit
    }


    private fun thread(format: String) = ThreadFactoryBuilder().setNameFormat(format).build()

    fun close() {
        println("Starting shutdown...")
        context.cancel(null)
        streamExecutor.shutdown()
        streamExecutor.awaitTermination(10, TimeUnit.SECONDS)
        processExecutor.shutdown()
        processExecutor.awaitTermination(20, TimeUnit.SECONDS)
        channel.shutdown().awaitTermination(10, TimeUnit.SECONDS)
        println("Shutdown complete.")
    }
}

class BearerToken(private val tokenSupplier: Supplier<String>) : CallCredentials() {
    override fun applyRequestMetadata(
        requestInfo: RequestInfo, executor: Executor, metadataApplier: MetadataApplier
    ) {
        executor.execute {
            try {
                val headers = Metadata()
                val token = tokenSupplier.get()
                headers.put(AUTH_HEADER, "Bearer $token")
                metadataApplier.apply(headers)
            } catch (e: Throwable) {
                metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e))
            }
        }
    }

    override fun thisUsesUnstableApi() {}

    companion object {
        private val AUTH_HEADER = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

Example result

{
"metadata": {
  "timestamp": "2022-08-09T12:22:57.110Z",
  "identifier": {
    "subscriptionIdentifier": {
      "value": "490e6f4a2cccd6067741b8a2a4bb14b2a1b2756c523c6c818e7eaee651b07743e69383304f45b3f6970b8bd01d16f845bc999d1ff036a5e605fec314d8aed99e"
    }
  },
  "ackInfo": {
    "value": "$JS.ACK.consent_stream.H5yg2gnj.1.9.1.1660047797010497111.0"
  }
},
"consentChangeEvent": {
  "added": {
    "scopes": [ "sms.text:send_to_subscriber" ]
  },
  "number": {
    "e164": "+46724452050"
  }
}
}

{
"metadata": {
  "timestamp": "2022-08-09T12:23:17.010Z",
  "identifier": {
    "subscriptionIdentifier": {
      "value": "490e6f4a2cccd6067741b8a2a4bb14b2a1b2756c523c6c818e7eaee651b07743e69383304f45b3f6970b8bd01d16f845bc999d1ff036a5e605fec314d8aed99e"
    }
  },
  "ackInfo": {
    "value": "$JS.ACK.consent_stream.H5yg2gnj.1.9.1.1660047797010497633.0"
  }
},
"consentChangeEvent": {
  "revoked": {

  },
  "number": {
    "e164": "+46724452050"
  }
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

Read more